<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【測(cè)試】680- 京喜前端自動(dòng)化測(cè)試之路

          共 10544字,需瀏覽 22分鐘

           ·

          2020-08-10 08:56


          前言

          京喜(原京東拼購)項(xiàng)目,作為京東戰(zhàn)略級(jí)業(yè)務(wù),擁有千萬級(jí)別的流量入口。為了保障線上業(yè)務(wù)的穩(wěn)定運(yùn)行,每月例行開展前端容災(zāi)演習(xí),主要包含小程序及 H5 版本,要求各頁面各模塊在異常情況下進(jìn)行適當(dāng)?shù)慕导?jí)處理,不能出現(xiàn)空窗、樣式錯(cuò)亂、不合理的錯(cuò)誤提示等體驗(yàn)問題。

          原來的容災(zāi)演習(xí)過程:小程序(通信方式改成 HTTPS )和 H5 通過 Whistle 對(duì)接口返回進(jìn)行修改來模擬異常情況,驗(yàn)證各頁面各模塊的降級(jí)處理符合預(yù)期。

          容災(zāi)演習(xí)是一項(xiàng)長期持續(xù)的工作,且涉及頁面功能及場(chǎng)景多,人工的切換場(chǎng)景模擬異常導(dǎo)致演習(xí)效率很低,因此想通過開發(fā)自動(dòng)化測(cè)試工具來提升研發(fā)效率,讓容災(zāi)演習(xí)工作隨時(shí)可以輕松開展。京喜 H5 和小程序場(chǎng)景差異比較大,因此自動(dòng)化測(cè)試之路分 H5 和小程序兩部分進(jìn)行,以 H5 作為一個(gè)開篇。

          綜上所述,我們希望京喜 H5 自動(dòng)化測(cè)試工具可以提供以下功能:

          1. 訪問目標(biāo)頁面,對(duì)頁面進(jìn)行截圖;
          2. 設(shè)置 UA(模擬不同渠道:微信、手Q、其它瀏覽器等);
          3. 模擬用戶點(diǎn)擊、滑動(dòng)頁面操作;
          4. 網(wǎng)絡(luò)攔截、模擬異常情況(接口響應(yīng)碼 500、接口返回?cái)?shù)據(jù)異常);
          5. 操作緩存數(shù)據(jù)(模擬有無緩存的場(chǎng)景等)。

          技術(shù)選型

          提到 Web 的自動(dòng)化測(cè)試,很多人熟悉的是 Selenium 2.0(Selenium WebDriver), 支持多平臺(tái)、多語言、多款瀏覽器(通過各種瀏覽器的驅(qū)動(dòng)來驅(qū)動(dòng)瀏覽器),提供了功能豐富的API接口。而隨著前端技術(shù)的發(fā)展,Selenium 2.0 逐漸呈現(xiàn)出環(huán)境安裝復(fù)雜、API 調(diào)用不友好、性能不高等缺點(diǎn)。新一代的自動(dòng)化測(cè)試工具 —— Puppeteer ,相較于 Selenium WebDriver 環(huán)境安裝更簡單、性能更好、效率更高、在瀏覽器執(zhí)行 Javascript 的 API 更簡單,它還提供了網(wǎng)絡(luò)攔截等功能。

          Puppeteer[1]?是一個(gè) Node 庫,它提供了一套高階 API ,通過 Devtools 協(xié)議控制?Chromium?或?Chrome?瀏覽器。Puppeteer?默認(rèn)以?Headless?模式運(yùn)行,但是可以通過修改配置文件運(yùn)行“有頭”模式。

          官方描述的功能:

          • 生成頁面 PDF;
          • 抓取 SPA(單頁應(yīng)用)并生成預(yù)渲染內(nèi)容(即“ SSR ”,服務(wù)器端渲染);
          • 自動(dòng)提交表單,進(jìn)行 UI 測(cè)試,鍵盤輸入等;
          • 創(chuàng)建一個(gè)時(shí)時(shí)更新的自動(dòng)化測(cè)試環(huán)境,使用 JavaScript 和最新的瀏覽器功能直接在最新版本的 Chrome 中執(zhí)行測(cè)試;
          • 捕獲網(wǎng)站的 Timeline Trace,用來幫助分析性能問題;
          • 測(cè)試瀏覽器擴(kuò)展。

          Puppeteer 提供了一種啟動(dòng) Chromium 實(shí)例的方法。當(dāng) Puppeteer 連接到一個(gè) Chromium 實(shí)例的時(shí)候會(huì)通過 puppeteer.launch 或 puppeteer.connect 創(chuàng)建一個(gè) Browser 對(duì)象,再通過 Browser 創(chuàng)建一個(gè) Page 實(shí)例,導(dǎo)航到一個(gè) Url ,然后保存截圖。一個(gè) Browser 實(shí)例可以有多個(gè) Page 實(shí)例。下面就是使用 Puppeteer 進(jìn)行自動(dòng)化的一個(gè)典型示例:

          const puppeteer = require('puppeteer');
          puppeteer.launch().then(async browser => {
          const page = await browser.newPage();
          await page.goto('https://example.com');
          await page.screenshot({path: 'screenshot.png'});
          await browser.close();
          });

          綜上所述,我們選擇基于 Puppeteer 來開發(fā)京喜首頁容災(zāi)演習(xí)的自動(dòng)化測(cè)試工具,通過 Puppeteer 提供的一系列 API ,實(shí)現(xiàn)訪問目標(biāo)頁面、模擬異常場(chǎng)景、生成截圖的過程自動(dòng)化。最后再通過人工比對(duì)截圖,判斷頁面降級(jí)處理是否符合預(yù)期、用戶體驗(yàn)是否友好。

          實(shí)現(xiàn)方案

          我們將容災(zāi)演習(xí)過程分為自動(dòng)化流程和人工操作兩部分。

          自動(dòng)化流程:

          1. 模擬用戶訪問頁面操作;
          2. 攔截網(wǎng)絡(luò)請(qǐng)求,修改接口返回?cái)?shù)據(jù),模擬異常場(chǎng)景(接口返回 500、異常數(shù)據(jù)等);
          3. 生成截圖。

          人工操作:

          自動(dòng)化腳本執(zhí)行完畢后,人工比對(duì)各個(gè)場(chǎng)景的截圖,判斷是否符合預(yù)期。

          方案流程圖:

          開發(fā)實(shí)錄

          安裝 Puppeteer ,你可能會(huì)遇到的那些事

          通過 npm init 初始化項(xiàng)目后, 就可以安裝 Puppeteer 依賴了:

          npm i puppeteer?:在安裝時(shí)自動(dòng)下載最新版本 Chromium。

          或者

          npm i puppeteer-core?:在安裝時(shí)不會(huì)自動(dòng)下載 Chromium。(不能生成截圖)

          另外,在安裝過程中可能會(huì)因?yàn)橄螺d Chromium 導(dǎo)致報(bào)錯(cuò),官網(wǎng)建議是先通過 ?npm i --save puppeteer --ignore-scripts?阻止下載 Chromium, 然后再手動(dòng)下載?Chromium[2]?。

          手動(dòng)下載后,需要配置指定路徑,修改 index.js 文件

          const puppeteer = require('puppeteer');
          (async () => {
          const browser = await puppeteer.launch({
          // 運(yùn)行 Chromium 或 Chrome 可執(zhí)行文件的路徑(相對(duì)路徑)
          executablePath: './chrome-mac/Chromium.app/Contents/MacOS/Chromium',
          headless: false
          });
          const page = await browser.newPage();
          await page.goto('https://example.com');
          await page.screenshot({path: 'screenshot.png'});
          browser.close();
          })();

          快速創(chuàng)建測(cè)試用例

          為了提高測(cè)試腳本的可維護(hù)性、擴(kuò)展性,我們將測(cè)試用例的信息都配置到 JSON 文件中,這樣編寫測(cè)試腳本的時(shí)候,我們只需關(guān)注測(cè)試流程的實(shí)現(xiàn)。

          測(cè)試用例 JSON 數(shù)據(jù)配置包括公用數(shù)據(jù)(global)私有數(shù)據(jù)

          公用數(shù)據(jù)(global):各測(cè)試用例都需要用到的數(shù)據(jù),如:模擬訪問的目標(biāo)頁面地址、名字、描述、設(shè)備類型等。

          私有數(shù)據(jù):各測(cè)試用例特定的數(shù)據(jù),如測(cè)試模塊信息、API 地址、測(cè)試場(chǎng)景、預(yù)期結(jié)果、截圖名字等數(shù)據(jù)。

          {
          "global": {
          "url": "https://wqs.jd.com/xxx/index.shtml",
          "pageName": "index",
          "pageDesc": "首頁",
          "device": "iPhone 7"
          },
          "homePageApi": {
          "id": 1,
          "module": "home_page_api",
          "moduleDesc": "首頁主接口",
          "api": "https://wqcoss.jd.com/xxx",
          "operation": "模擬響應(yīng)碼 500",
          "expectRules": [
          "1. 顯示異常信息、刷新按鈕",
          "2. 點(diǎn)擊刷新按鈕,顯示異常信息",
          "3. 恢復(fù)網(wǎng)絡(luò),點(diǎn)擊刷新按鈕,顯示正常數(shù)據(jù)"
          ],
          "screenshot": [
          {
          "name": "normal",
          "desc": "正常場(chǎng)景"
          },
          {
          "name": "500_cache",
          "desc": "有緩存-返回500"
          },
          {
          "name": "500_no_cache",
          "desc": "無緩存-返回500"
          },
          {
          "name": "500_no_cache_reload",
          "desc": "無緩存-返回500-點(diǎn)擊刷新按鈕"
          },
          {
          "name": "500_no_cache_recover",
          "desc": "無緩存-返回500-恢復(fù)網(wǎng)絡(luò)"
          }
          ]
          },

          }

          編寫測(cè)試腳本

          我們以京喜首頁主接口的測(cè)試用例為例子,通過模接口返回 500 響應(yīng)碼的異常場(chǎng)景,驗(yàn)證主接口的異常處理機(jī)制是否完善、用戶體驗(yàn)是否友好。

          預(yù)期效果:

          • 有緩存情況下,顯示緩存數(shù)據(jù)
          • 無緩存情況下顯示異常信息、刷新按鈕
          • 點(diǎn)擊刷新按鈕,顯示異常信息
          • 恢復(fù)網(wǎng)絡(luò),點(diǎn)擊刷新按鈕,顯示正常數(shù)據(jù)

          測(cè)試流程:

          方案流程圖

          場(chǎng)景實(shí)現(xiàn):

          根據(jù)測(cè)試流程以及配置的測(cè)試用例信息,編寫測(cè)試腳本,實(shí)現(xiàn)測(cè)試用例場(chǎng)景:

          1. 訪問頁面
          await page.goto(url)
          1. 生成截圖
          await page.screenshot({
          path: './screenshot/index_home_page_500.png'
          })

          1. 攔截接口請(qǐng)求
          async test () => {
          ... // 創(chuàng)建 Page 實(shí)例,訪問首頁
          await page.setRequestInterception(true) // 設(shè)置攔截請(qǐng)求
          page.on("request", interceptionEvent) // 監(jiān)聽請(qǐng)求事件,當(dāng)請(qǐng)求發(fā)起后頁面會(huì)觸發(fā)這個(gè)事件
          ... // 刷新頁面,觸發(fā)請(qǐng)求攔截,生成測(cè)試場(chǎng)景截圖
          }

          若測(cè)試用例需要攔截不同的請(qǐng)求,或是模擬多種場(chǎng)景,則需要設(shè)置多個(gè)請(qǐng)求監(jiān)聽事件。且一個(gè)事件執(zhí)行結(jié)束后,必須要移除事件監(jiān)聽,才能繼續(xù)下一個(gè)事件監(jiān)聽。

          添加事件監(jiān)聽:page.on("request", eventFunction)

          移除事件監(jiān)聽:page.off("request", eventFunction)

          // 設(shè)置攔截請(qǐng)求
          await page.setRequestInterception(true)
          const iconInterception1 = requestInterception(api, "body")
          // 添加事件 1 監(jiān)聽
          page.on("request", iconInterception1)
          await page.goto(url)
          await page.screenshot({
          path: './screenshot/1.png'
          })
          // 移除事件 1 監(jiān)聽
          page.off("request", iconInterception1)
          const iconInterception2 = requestInterception(api, "body", )
          // 添加事件 2 監(jiān)聽
          page.on("request", iconInterception2)
          await page.goto(url)
          await page.screenshot({
          path: './screenshot/2.png'
          })
          // 移除事件 2 監(jiān)聽
          page.off("request", iconInterception2)
          1. 模擬異常數(shù)據(jù)場(chǎng)景,生成 mock 數(shù)據(jù)。
          function requestInterception (api, setProps, setValue) {
          let mockData
          switch (setProps) {
          case"status": // 修改返回狀態(tài)碼
          mockData = {
          status: setValue
          }
          break
          case"contentType": // 修改返回內(nèi)容類型
          mockData = {
          contentType: setValue
          }
          break
          case"body": // 修改返回?cái)?shù)據(jù)
          mockData = {
          contentType: getMockResponse(setValue)
          }
          break
          default:
          break
          }
          returnasync req => {
          // 如果是需要攔截的 API,則通過 req.respond(mockData) 修改返回?cái)?shù)據(jù),否則 continue 繼續(xù)請(qǐng)求別的
          if (req.url().includes(api)) { // 攔截 API
          req.respond(mockData) // 修改返回?cái)?shù)據(jù)
          returnfalse// 處理完了某個(gè)請(qǐng)求必須退出,不再執(zhí)行 continue
          }
          req.continue()
          }

          模擬接口返回 500:

          const interception500 = requestInterception(api, 'status', 500)
          page.on("request", interception500) // 當(dāng)請(qǐng)求發(fā)起后頁面會(huì)觸發(fā)這個(gè)事件

          模擬異常數(shù)據(jù):

          const iconInterception = requestInterception(api, "body", {
          "data": {
          "modules": [{
          "tpl": "3000",
          "content": []
          }]
          }
          })
          page.on("request", iconInterception)

          生成 mock 數(shù)據(jù)有兩種實(shí)現(xiàn)方案,可依據(jù)實(shí)際情況而定:

          • [ ] 直接通過修改接口真實(shí)返回的數(shù)據(jù)生成 mock 數(shù)據(jù),需要先獲取接口實(shí)時(shí)返回?cái)?shù)據(jù)
          • [x] 本地存儲(chǔ)一份完整的接口數(shù)據(jù),通過修改本地存儲(chǔ)數(shù)據(jù)的方式生成 mock 數(shù)據(jù)(本文所述案例均基于此方案實(shí)現(xiàn))

          若選擇第一種方案,則需先攔截接口請(qǐng)求,通過 req.response() 獲取接口實(shí)時(shí)返回?cái)?shù)據(jù),根據(jù)測(cè)試場(chǎng)景修改實(shí)時(shí)返回?cái)?shù)據(jù)作為 mock 數(shù)據(jù)。

          由于京喜 H5 頁面接口返回是 JSONP 格式的數(shù)據(jù),所以在模擬返回?cái)?shù)據(jù)的時(shí)候,必須先截取 JSONP 的 callback 信息,與模擬數(shù)據(jù)拼接后再返回;

          function requestInterception (api, setProps, setValue) {
          let mockData
          switch (setProps) {
          case"status":
          mockData = {
          status: setValue
          }
          break
          case"contentType":
          mockData = {
          contentType: setValue
          }
          break
          default:
          break
          }
          returnasync req => {
          if (req.url().includes(api)) {
          if (setProps === "body") {
          const callback = getUrlParam("callback", req.url()) // 獲取 callback 信息
          const localData = getLocalMockResponse(api) // 匹配 API ,獲取本地存儲(chǔ)數(shù)據(jù)
          mockData = {
          body: getResponseMockLocalData(localData, setValue, callback, api) // 生成 mock 數(shù)據(jù)
          }
          }
          req.respond(mockData) // 設(shè)置返回?cái)?shù)據(jù)
          returnfalse
          }
          req.continue()
          }
          }
          1. 清除緩存
          page.evaluate(() => {
          try {
          localStorage.clear()
          sessionStorage.clear()
          } catch (e) {
          console.log(e)
          }
          })
          1. 點(diǎn)擊刷新按鈕
          await page.waitFor(".page-error__refresh-btn") // 可以傳 CSS 選擇器,也可以傳時(shí)間(單位毫秒)
          await page.click(".page-error__refresh-btn")

          在模擬點(diǎn)擊刷新按鈕之前,需等待按鈕渲染完成,再觸發(fā)按鈕點(diǎn)擊。(防止刷新頁面后,DOM 還未渲染完成的情況下,因找不到 DOM 導(dǎo)致報(bào)錯(cuò))

          1. 取消攔截,恢復(fù)網(wǎng)絡(luò)
          await page.setRequestInterception(false)

          運(yùn)行腳本及調(diào)試

          由于第一階段的測(cè)試工具尚未平臺(tái)化,自動(dòng)化測(cè)試流程先通過在終端輸入命令行,運(yùn)行腳本的方式啟動(dòng)。

          在項(xiàng)目的 package.json 文件中,使用 scripts 字段定義腳本命令:

          "scripts": {
          "test:real": "node ./pages/index/index.js",
          "test:mock": "node ./pages/index-mock/index.js"
          },

          運(yùn)行:

          在終端切入到項(xiàng)目根目錄路徑,輸入以下命令行,就可以啟動(dòng)測(cè)試工具,運(yùn)行測(cè)試腳本。

          - npm run test:real // 接口真實(shí)返回的數(shù)據(jù)測(cè)試
          - npm run test:mock // 使用本地 mock 數(shù)據(jù)測(cè)試

          調(diào)試:

          開啟調(diào)試模式之前,需要先了解?Headless Chrome

          Headless Chrome?,無頭模式,瀏覽器的無界面形態(tài),可以在不打開瀏覽器的前提下,在命令行中運(yùn)行測(cè)試腳本,能夠完全像真實(shí)瀏覽器一樣完成用戶所有操作,不用擔(dān)心運(yùn)行測(cè)試腳本時(shí)瀏覽器受到外界的干擾,也不需要借助任何顯示設(shè)備,使自動(dòng)化測(cè)試更穩(wěn)定。

          Puppeteer?默認(rèn)以無頭模式運(yùn)行。

          那么要開啟調(diào)試模式,就必須取消無頭模式,在打開瀏覽器的場(chǎng)景下,進(jìn)行自動(dòng)化測(cè)試。因此,在命令行腳本中增加了“取消無頭模式”和“打開開發(fā)者工具”的參數(shù),測(cè)試腳本通過獲取到的參數(shù),決定是否開啟調(diào)試模式。

          const headless = process.argv[2] !== 'head'// 獲取是否開啟無頭模式參數(shù)
          const devtools = process.argv[3] === 'dev'// 獲取是否打開開發(fā)者工具參數(shù)
          const browser = await puppeteer.launch({
          executablePath: browserPath,
          headless,
          devtools
          })

          在終端切入到項(xiàng)目根目錄路徑,輸入以下命令行,就可以開啟調(diào)試模式,運(yùn)行測(cè)試腳本。

          - npm run test:mock head // 打開 Chromium 窗口
          - npm run test:mock head dev // 打開 Chromium 窗口 和 開發(fā)者工具窗口
          • head?參數(shù):取消無頭模式,打開 Chromium 窗口運(yùn)行腳本;
          • head dev?參數(shù):在打開 Chromium 窗口運(yùn)行腳本,并打開 Devtools 窗口,開啟調(diào)試模式。

          測(cè)試結(jié)果

          人工比對(duì)截圖結(jié)果:

          運(yùn)行腳本示例:

          方案流程圖

          更多測(cè)試場(chǎng)景實(shí)現(xiàn)

          1. 截取從頁面頂部到指定 DOM 之間的區(qū)域(內(nèi)容可能超出一屏的長圖)

          Puppeteer 提供了四種截圖方式:

          (1)截取一屏內(nèi)容(默認(rèn)普通截屏);
          (2)截取指定 DOM;
          (3)截取全屏;
          (4)指定裁剪區(qū)域,可設(shè)置 x、y、width、height。x, y 是相對(duì)頁面左上角。但只能截取一屏的內(nèi)容,超出一屏不展示。

          基于第四種方法進(jìn)行改造:

          1. 通過原生 JavaScript 的 getBoundingClientRect() 方法獲取到指定 DOM 的 x,y 坐標(biāo)值;
          2. 通過 page.setViewport() 重置視口的高度;
          3. 調(diào)用截圖 API 生成截圖。
          asyncfunction screenshotToElement (page, selector, path) {
          try {
          await page.waitForSelector(selector)
          let clip = await page.evaluate(selector => {
          const element = document.querySelector(selector)
          let { x, y, width, height } = element.getBoundingClientRect()
          return {
          x: 0,
          y: 0,
          width,
          height: M(y),
          }
          }, selector)
          await page.setViewport(clip)
          await page.screenshot({
          path: path,
          clip: clip
          })
          } catch (e) {
          console.log(e)
          }
          }
          • height: y?:截到指定 DOM 的頂部,不包含該 DOM;
          • height: y + height?:截到指定 DOM 的底部,包含該 DOM;
          • 原生 Javascript 的 getBoundingClientRect() 方法獲取 DOM 元素定位和寬高值可能是小數(shù),而 Puppeteer 的 setViewport() 設(shè)置視口方法不支持小數(shù),所以需要對(duì)獲取到的 DOM 元素定位信息取整。

          2. 模擬不同渠道,如:手Q場(chǎng)景:

          // 設(shè)置 UA
          await page.setUserAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 10_2_1 like Mac OS X) AppleWebKit/602.4.6 (KHTML, like Gecko) Mobile/14D27 QQ/6.7.1.416 V1_IPH_SQ_6.7.1_1_APP_A Pixel/750 Core/UIWebView NetType/4G QBWebViewType/1")

          3. 滾動(dòng)頁面

          await page.evaluate((top) => {
          window.scrollTo(0, top)
          }, top)

          page.evaluate(pageFunction, …args):在當(dāng)前頁面實(shí)例上下文中執(zhí)行 ?JavaScript 代碼

          4. 監(jiān)聽頁面崩潰事件

          // 當(dāng)頁面崩潰時(shí)觸發(fā)
          page.on('error', (e) => {
          console.log(e)
          })

          結(jié)語

          第一階段的 H5 自動(dòng)化之路告一段落,容災(zāi)演習(xí)已實(shí)現(xiàn)了半自動(dòng)化,可通過在終端運(yùn)行測(cè)試腳本,模擬異常場(chǎng)景自動(dòng)生成截圖,再配合人工比對(duì)截圖操作,判斷演習(xí)結(jié)果是否符合預(yù)期。目前已投入到每個(gè)月的容災(zāi)演習(xí)中使用。

          隨著京喜業(yè)務(wù)的迭代,頁面也將更新改版,因此測(cè)試用例也需要持續(xù)維護(hù)和更新。后續(xù)將持續(xù)優(yōu)化自動(dòng)化工具,共享測(cè)試腳本、在生成截圖的基礎(chǔ)上自動(dòng)比對(duì)測(cè)試結(jié)果是否符合預(yù)期、數(shù)據(jù)入庫、將測(cè)試結(jié)果轉(zhuǎn)化成文檔,自動(dòng)發(fā)送郵件等等。基于容災(zāi)演習(xí)的自動(dòng)化測(cè)試,還可擴(kuò)展廣告位的監(jiān)測(cè),數(shù)據(jù)上報(bào)監(jiān)聽自動(dòng)化測(cè)試……

          對(duì)于京喜首頁自動(dòng)化測(cè)試之路,遠(yuǎn)沒有結(jié)束,還有很多可以優(yōu)化和擴(kuò)展的地方,接下來分階段持續(xù)優(yōu)化自動(dòng)化測(cè)試工具,敬請(qǐng)期待!

          參考資料

          [1]

          Puppeteer:?https://zhaoqize.github.io/puppeteer-api-zh_CN/

          [2]

          Chromium:?https://download-chromium.appspot.com/




          1. JavaScript 重溫系列(22篇全)
          2. ECMAScript 重溫系列(10篇全)
          3. JavaScript設(shè)計(jì)模式 重溫系列(9篇全)
          4.?正則 / 框架 / 算法等 重溫系列(16篇全)
          5.?Webpack4 入門(上)||?Webpack4 入門(下)
          6.?MobX 入門(上)?||??MobX 入門(下)
          7.?70+篇原創(chuàng)系列匯總

          回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~

          點(diǎn)擊“閱讀原文”查看70+篇原創(chuàng)文章

          點(diǎn)這,與大家一起分享本文吧~
          瀏覽 39
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  操女生网站 | 天堂中文在线a | 日韩三级毛片 | 国产精品免费一区二区三区 | 青草青草久久 |