ข้อเสนอแนะสำหรับการทดสอบโค้ด extjs ในเบราว์เซอร์ควรใช้ซีลีเนียมหรือไม่


92

เราใช้ซีลีเนียมประสบความสำเร็จอย่างมากในการจัดการกับการทดสอบเว็บไซต์ระดับสูง (นอกเหนือจากการทดสอบเกี่ยวกับงูหลามในระดับโมดูล) อย่างไรก็ตามตอนนี้เรากำลังใช้ extjs สำหรับเพจจำนวนมากและพิสูจน์ได้ยากที่จะรวมการทดสอบซีลีเนียมสำหรับส่วนประกอบที่ซับซ้อนเช่นกริด

มีใครประสบความสำเร็จในการเขียนการทดสอบอัตโนมัติสำหรับหน้าเว็บที่ใช้ extjs บ้าง? Googling จำนวนมากพบผู้ที่มีปัญหาคล้ายกัน แต่มีคำตอบเพียงไม่กี่คำ ขอบคุณ!


คนดีมากกว่าที่ Ext JS ได้รับชนิดพอที่จะโพสต์เกี่ยวกับเรื่องนี้มากในบล็อกของพวกเขาที่นี่ ฉันหวังว่านี่จะช่วยได้.
NBRed5

คำตอบ:


173

อุปสรรคที่ใหญ่ที่สุดในการทดสอบ ExtJS กับ Selenium คือ ExtJS ไม่แสดงองค์ประกอบ HTML มาตรฐานและ Selenium IDE จะสร้างคำสั่งที่กำหนดเป้าหมายไปที่องค์ประกอบที่ทำหน้าที่เป็นเพียงการตกแต่ง - องค์ประกอบที่ไม่จำเป็นซึ่งช่วย ExtJS กับเดสก์ท็อปทั้งหมด - มองและรู้สึก. นี่คือเคล็ดลับและกลเม็ดบางส่วนที่ฉันรวบรวมขณะเขียนการทดสอบซีลีเนียมอัตโนมัติกับแอป ExtJS

เคล็ดลับทั่วไป

การค้นหาองค์ประกอบ

เมื่อสร้างกรณีทดสอบ Selenium โดยบันทึกการกระทำของผู้ใช้ด้วย Selenium IDE บน Firefox ซีลีเนียมจะยึดการกระทำที่บันทึกไว้บนรหัสขององค์ประกอบ HTML อย่างไรก็ตามสำหรับองค์ประกอบที่คลิกได้ส่วนใหญ่ ExtJS จะใช้รหัสที่สร้างขึ้นเช่น "ext-gen-345" ซึ่งมีแนวโน้มที่จะเปลี่ยนแปลงในการเข้าชมหน้าเดียวกันในภายหลังแม้ว่าจะไม่มีการเปลี่ยนแปลงโค้ดก็ตาม หลังจากบันทึกการดำเนินการของผู้ใช้สำหรับการทดสอบแล้วจำเป็นต้องใช้ความพยายามด้วยตนเองในการดำเนินการดังกล่าวทั้งหมดซึ่งขึ้นอยู่กับรหัสที่สร้างขึ้นและเพื่อแทนที่ มีการเปลี่ยนสองประเภทที่สามารถทำได้:

การแทนที่ Id Locator ด้วย CSS หรือ XPath Locator

ตัวระบุตำแหน่ง CSS เริ่มต้นด้วย "css =" และ XPath locators ขึ้นต้นด้วย "//" (คำนำหน้า "xpath =" เป็นทางเลือก) ตัวระบุตำแหน่ง CSS มีรายละเอียดน้อยกว่าและอ่านง่ายกว่าและควรเป็นที่ต้องการมากกว่าตัวระบุตำแหน่ง XPath อย่างไรก็ตามอาจมีบางกรณีที่จำเป็นต้องใช้ตัวระบุตำแหน่ง XPath เนื่องจากตัวระบุตำแหน่ง CSS ไม่สามารถตัดมันได้

การเรียกใช้งาน JavaScript

องค์ประกอบบางอย่างต้องการการโต้ตอบของเมาส์ / คีย์บอร์ดมากกว่าปกติเนื่องจากการแสดงผลที่ซับซ้อนซึ่งดำเนินการโดย ExtJS ตัวอย่างเช่น Ext.form.CombBox ไม่ใช่<select>องค์ประกอบจริง ๆแต่เป็นอินพุตข้อความที่มีรายการแบบเลื่อนลงแยกออกซึ่งอยู่ที่ด้านล่างของโครงสร้างเอกสาร ในการจำลองการเลือก ComboBox อย่างถูกต้องคุณสามารถจำลองการคลิกที่ลูกศรแบบเลื่อนลงก่อนจากนั้นจึงคลิกรายการที่ปรากฏขึ้น อย่างไรก็ตามการค้นหาองค์ประกอบเหล่านี้ผ่านตัวระบุตำแหน่ง CSS หรือ XPath อาจเป็นเรื่องยุ่งยาก อีกทางเลือกหนึ่งคือค้นหาคอมโพเนนต์ ComoBox และเรียกใช้วิธีการเพื่อจำลองการเลือก:

var combo = Ext.getCmp('genderComboBox'); // returns the ComboBox components
combo.setValue('female'); // set the value
combo.fireEvent('select'); // because setValue() doesn't trigger the event

ในซีลีเนียมrunScriptสามารถใช้คำสั่งเพื่อดำเนินการข้างต้นในรูปแบบที่กระชับยิ่งขึ้น:

with (Ext.getCmp('genderComboBox')) { setValue('female'); fireEvent('select'); }

การรับมือกับ AJAX และการแสดงผลช้า

ซีลีเนียมมีรสชาติ "* AndWait" สำหรับคำสั่งทั้งหมดสำหรับการรอการโหลดหน้าเมื่อการดำเนินการของผู้ใช้ส่งผลให้เกิดการเปลี่ยนหน้าหรือโหลดซ้ำ อย่างไรก็ตามเนื่องจากการดึงข้อมูล AJAX ไม่เกี่ยวข้องกับการโหลดหน้าเว็บจริงจึงไม่สามารถใช้คำสั่งเหล่านี้ในการซิงโครไนซ์ได้ วิธีแก้ปัญหาคือการใช้ประโยชน์จากเบาะแสภาพเช่นการมี / ไม่มีตัวบ่งชี้ความคืบหน้า AJAX หรือการปรากฏของแถวในตารางส่วนประกอบเพิ่มเติมลิงก์ ฯลฯ ตัวอย่างเช่น:

Command: waitForElementNotPresent
Target: css=div:contains('Loading...')

บางครั้งองค์ประกอบจะปรากฏขึ้นหลังจากผ่านไประยะหนึ่งแล้วขึ้นอยู่กับว่า ExtJS แสดงผลคอมโพเนนต์เร็วเพียงใดหลังจากการกระทำของผู้ใช้ส่งผลให้มุมมองเปลี่ยนไป แทนที่จะใช้pauseคำสั่งอนุญาโตตุลาการวิธีที่ดีที่สุดคือรอจนกว่าองค์ประกอบที่น่าสนใจจะเข้ามาในความเข้าใจของเรา ตัวอย่างเช่นหากต้องการคลิกที่รายการหลังจากรอให้รายการนั้นปรากฏ:

Command: waitForElementPresent
Target: css=span:contains('Do the funky thing')
Command: click
Target: css=span:contains('Do the funky thing')

การใช้การหยุดชั่วคราวโดยพลการไม่ใช่ความคิดที่ดีเนื่องจากความแตกต่างของเวลาซึ่งเป็นผลมาจากการเรียกใช้การทดสอบในเบราว์เซอร์ที่แตกต่างกันหรือในเครื่องต่าง ๆ จะทำให้กรณีทดสอบไม่สม่ำเสมอ

รายการที่ไม่สามารถคลิกได้

clickคำสั่งไม่สามารถเรียกใช้องค์ประกอบบางอย่างได้ เป็นเพราะตัวฟังเหตุการณ์อยู่บนคอนเทนเนอร์จริง ๆ โดยเฝ้าดูเหตุการณ์ของเมาส์ในองค์ประกอบลูกของมันซึ่งในที่สุดก็ส่งถึงผู้ปกครอง ตัวควบคุมแท็บเป็นตัวอย่างหนึ่ง ในการคลิกที่แท็บคุณต้องจำลองmouseDownเหตุการณ์ที่ป้ายกำกับแท็บ:

Command: mouseDownAt
Target: css=.x-tab-strip-text:contains('Options')
Value: 0,0

การตรวจสอบฟิลด์

ช่องฟอร์ม (ส่วนประกอบ Ext.form. *) ที่เชื่อมโยงกับนิพจน์ทั่วไปหรือ vtypes สำหรับการตรวจสอบความถูกต้องจะทริกเกอร์การตรวจสอบความถูกต้องด้วยความล่าช้าบางอย่าง (ดูvalidationDelayคุณสมบัติที่กำหนดเป็น 250ms ตามค่าเริ่มต้น) หลังจากผู้ใช้ป้อนข้อความหรือทันทีเมื่อฟิลด์สูญเสีย โฟกัส - หรือพร่ามัว (ดูvalidateOnDelayคุณสมบัติ) ในการเรียกใช้การตรวจสอบความถูกต้องของฟิลด์หลังจากออกคำสั่ง type Selenium เพื่อป้อนข้อความภายในฟิลด์คุณต้องดำเนินการอย่างใดอย่างหนึ่งต่อไปนี้:

  • ทริกเกอร์การตรวจสอบความถูกต้องล่าช้า

    ExtJS เริ่มการทำงานของตัวจับเวลาการหน่วงเวลาการตรวจสอบเมื่อฟิลด์ได้รับเหตุการณ์คีย์อัพ ในการทริกเกอร์ตัวจับเวลานี้เพียงแค่ออกเหตุการณ์การคีย์ดัมมี่ (ไม่สำคัญว่าคุณจะใช้คีย์ใดเนื่องจาก ExtJS จะละเว้น) ตามด้วยการหยุดชั่วคราวสั้น ๆ ที่ยาวกว่า validationDelay:

    Command: keyUp
    Target: someTextArea
    Value: x
    Command: pause
    Target: 500
    
  • ทริกเกอร์การตรวจสอบความถูกต้องทันที

    คุณฉีดเหตุการณ์เบลอลงในช่องเพื่อเริ่มการตรวจสอบได้ทันที:

    Command: runScript
    Target: someComponent.nameTextField.fireEvent("blur")
    

กำลังตรวจสอบผลการตรวจสอบ

หลังจากการตรวจสอบความถูกต้องคุณสามารถตรวจสอบว่ามีหรือไม่มีฟิลด์ข้อผิดพลาด:

Command: verifyElementNotPresent   
Target: //*[@id="nameTextField"]/../*[@class="x-form-invalid-msg" and not(contains(@style, "display: none"))]

Command: verifyElementPresent   
Target: //*[@id="nameTextField"]/../*[@class="x-form-invalid-msg" and not(contains(@style, "display: none"))]

โปรดทราบว่าการตรวจสอบ "display: none" เป็นสิ่งที่จำเป็นเนื่องจากเมื่อช่องแสดงข้อผิดพลาดปรากฏขึ้นและจำเป็นต้องซ่อนไว้ ExtJS จะซ่อนช่องข้อผิดพลาดแทนที่จะลบออกจากโครงสร้าง DOM ทั้งหมด

เคล็ดลับเฉพาะองค์ประกอบ

การคลิกที่ปุ่มแบบฟอร์ม

  • ตัวเลือกที่ 1

    Command: คลิก Target: css = button: contains ('Save')

    เลือกปุ่มตามคำอธิบายภาพ

  • ทางเลือกที่ 2

    Command: คลิก Target: css = # ปุ่ม save-options

    เลือกปุ่มตามรหัส

การเลือกค่าจาก Ext.form.ComboBox

Command: runScript
Target: with (Ext.getCmp('genderComboBox')) { setValue('female'); fireEvent('select'); }

ก่อนอื่นให้ตั้งค่าจากนั้นจึงยิงเหตุการณ์ที่เลือกอย่างชัดเจนในกรณีที่มีผู้สังเกตการณ์


3
นี่คือเคล็ดลับคู่มือบางส่วน: rkapse.blogspot.nl/2009/06/selenium-ide-issues-with-extjs.html
dan-klasson

5

บล็อกนี้ช่วยฉันได้มาก เขาเขียนหัวข้อนี้ค่อนข้างมากและดูเหมือนว่าจะยังคงทำงานอยู่ ผู้ชายคนนี้ดูเหมือนจะชื่นชมการออกแบบที่ดี

โดยทั่วไปเขาพูดถึงการใช้การส่งจาวาสคริปต์เพื่อทำแบบสอบถามและใช้เมธอด Ext.ComponentQuery.query เพื่อดึงข้อมูลในลักษณะเดียวกับที่คุณทำในแอปเสริมของคุณภายใน ด้วยวิธีนี้คุณสามารถใช้ xtypes และ itemIds และไม่ต้องกังวลกับการพยายามแยกวิเคราะห์สิ่งที่สร้างขึ้นโดยอัตโนมัติอย่างบ้าคลั่ง

ฉันพบว่าบทความนี้มีประโยชน์มากโดยเฉพาะ

อาจโพสต์รายละเอียดเพิ่มเติมเกี่ยวกับที่นี่ในเร็ว ๆ นี้ - ยังคงพยายามทำความเข้าใจเกี่ยวกับวิธีการทำสิ่งนี้อย่างถูกต้อง


4

ฉันกำลังทดสอบเว็บแอปพลิเคชัน ExtJs กับซีลีเนียม ปัญหาที่ใหญ่ที่สุดอย่างหนึ่งคือการเลือกรายการในตารางเพื่อทำบางสิ่งกับมัน

สำหรับสิ่งนี้ฉันเขียนวิธีการช่วยเหลือ (ในคลาส SeleniumExtJsUtils ซึ่งเป็นชุดของวิธีการที่มีประโยชน์สำหรับการโต้ตอบกับ ExtJs ที่ง่ายขึ้น):

/**
 * Javascript needed to execute in order to select row in the grid
 * 
 * @param gridId Grid id
 * @param rowIndex Index of the row to select
 * @return Javascript to select row
 */
public static String selectGridRow(String gridId, int rowIndex) {
    return "Ext.getCmp('" + gridId + "').getSelectionModel().selectRow(" + rowIndex + ", true)";
}

และเมื่อฉันต้องการเลือกแถวฉันก็โทรหา:

selenium.runScript( SeleniumExtJsUtils.selectGridRow("<myGridId>", 5) );

เพื่อให้ใช้งานได้ฉันต้องตั้งรหัสของฉันบนกริดและไม่ให้ ExtJs สร้างขึ้นเอง


คุณควรจะสามารถค้นหาตารางได้โดยใช้ xtype ใน Ext.ComponentQuery.query (สมมติว่าคุณขยายจากกริดและกำหนด xtype ให้คุณเป็นเจ้าของกริด) ฉันได้ใส่ข้อมูลบางอย่างเกี่ยวกับสิ่งนี้ลงไป ฉันยังพบว่าคุณสามารถคลิกสิ่งต่างๆได้โดยใช้ xpath เพื่อค้นหา td ที่มีค่าระบุสำหรับแถว
JonnyRaa

4

ในการตรวจจับองค์ประกอบนั้นให้มองเห็นได้คุณใช้ประโยค: not(contains(@style, "display: none")

จะดีกว่าที่จะใช้สิ่งนี้:

visible_clause = "not(ancestor::*[contains(@style,'display: none')" +
    " or contains(@style, 'visibility: hidden') " + 
    " or contains(@class,'x-hide-display')])"

hidden_clause = "parent::*[contains(@style,'display: none')" + 
    " or contains(@style, 'visibility: hidden')" + 
    " or contains(@class,'x-hide-display')]"

3

คุณสามารถให้ข้อมูลเชิงลึกเพิ่มเติมเกี่ยวกับประเภทของปัญหาที่คุณประสบกับการทดสอบ extjs ได้หรือไม่?

หนึ่งนามสกุลซีลีเนียมฉันพบว่ามีประโยชน์เป็นwaitForCondition หากปัญหาของคุณดูเหมือนจะมีปัญหากับเหตุการณ์ Ajax คุณสามารถใช้ waitForCondition เพื่อรอให้เหตุการณ์เกิดขึ้น


3

หน้าเว็บ Ext JS อาจเป็นเรื่องยากในการทดสอบเนื่องจาก HTML ที่ซับซ้อนที่พวกเขาสร้างขึ้นด้วยกริด Ext JS

HTML5 Robotจัดการกับสิ่งนี้โดยใช้ชุดแนวทางปฏิบัติที่ดีที่สุดสำหรับวิธีการค้นหาและโต้ตอบกับส่วนประกอบอย่างน่าเชื่อถือตามคุณลักษณะและเงื่อนไขที่ไม่ใช่แบบไดนามิก จากนั้นจะมีทางลัดสำหรับการดำเนินการนี้กับส่วนประกอบ HTML, Ext JS และ Sencha Touch ทั้งหมดที่คุณต้องใช้ในการโต้ตอบ มีให้เลือก 2 รสชาติ:

  1. Java - API ที่ใช้ซีลีเนียมและ JUnit ที่คุ้นเคยซึ่งสร้างขึ้นในการรองรับไดรเวอร์เว็บสำหรับเบราว์เซอร์ที่ทันสมัยทั้งหมด
  2. Gwen - ภาษาสไตล์มนุษย์สำหรับการสร้างและบำรุงรักษาการทดสอบเบราว์เซอร์อย่างรวดเร็วและง่ายดายซึ่งมาพร้อมกับสภาพแวดล้อมการพัฒนาแบบบูรณาการของตัวเอง ซึ่งทั้งหมดนี้ใช้ Java API

ตัวอย่างเช่นหากคุณต้องการค้นหาแถวกริด Ext JS ที่มีข้อความ "Foo" คุณสามารถทำสิ่งต่อไปนี้ใน Java:

findExtJsGridRow("Foo");

... และคุณสามารถทำสิ่งต่อไปนี้ใน Gwen:

extjsgridrow by text "Foo"

มีเอกสารมากมายสำหรับทั้งJavaและ Gwen สำหรับวิธีการทำงานกับส่วนประกอบเฉพาะของ Ext JS เอกสารนี้ยังให้รายละเอียด HTML ที่เป็นผลลัพธ์สำหรับส่วนประกอบ Ext JS ทั้งหมดเหล่านี้ซึ่งคุณอาจพบว่ามีประโยชน์


2

เคล็ดลับที่เป็นประโยชน์ในการดึงข้อมูลตารางผ่าน Id of grid ในหน้า: ฉันคิดว่าคุณสามารถขยายฟังก์ชันที่มีประโยชน์เพิ่มเติมจาก API นี้ได้

   sub get_grid_row {
        my ($browser, $grid, $row)  = @_;


        my $script = "var doc = this.browserbot.getCurrentWindow().document;\n" .
            "var grid = doc.getElementById('$grid');\n" .
            "var table = grid.getElementsByTagName('table');\n" .
            "var result = '';\n" .
            "var row = 0;\n" . 
            "for (var i = 0; i < table.length; i++) {\n" .
            "   if (table[i].className == 'x-grid3-row-table') {\n".
            "       row++;\n" . 
            "       if (row == $row) {\n" .
            "           var cols_len = table[i].rows[0].cells.length;\n" .
            "           for (var j = 0; j < cols_len; j++) {\n" .
            "               var cell = table[i].rows[0].cells[j];\n" .
            "               if (result.length == 0) {\n" .
            "                   result = getText(cell);\n" .
            "               } else { \n" .
            "                   result += '|' + getText(cell);\n" .
            "               }\n" .
            "           }\n" .
            "       }\n" .
            "   }\n" .
            "}\n" .
            "result;\n";

        my $result = $browser->get_eval($script);
        my @res = split('\|', $result);
        return @res;
    }

2

ทดสอบได้ง่ายขึ้นผ่านแอตทริบิวต์ข้อมูล HTML ที่กำหนดเอง

จากเอกสาร Sencha :

itemId สามารถใช้เป็นทางเลือกอื่นในการรับการอ้างอิงไปยังส่วนประกอบเมื่อไม่มีการอ้างอิงวัตถุ แทนที่จะใช้ id กับ Ext.getCmp ให้ใช้ itemId กับ Ext.container.Container.getComponent ซึ่งจะดึงข้อมูล itemId หรือ id เนื่องจาก itemId เป็นดัชนีไปยัง MixedCollection ภายในของคอนเทนเนอร์ itemId จึงถูกกำหนดขอบเขตไว้ภายในคอนเทนเนอร์เพื่อหลีกเลี่ยงความขัดแย้งที่อาจเกิดขึ้นกับ Ext.ComponentManager ซึ่งต้องใช้รหัสเฉพาะ

เอาชนะExt.AbstractComponent's onBoxReadyวิธีผมตั้งแอตทริบิวต์ข้อมูลที่กำหนดเอง (ที่มีชื่อมาจากการกำหนดเองของฉันtestIdAttrคุณสมบัติของแต่ละองค์ประกอบ) องค์ประกอบของitemIdค่าถ้ามันมีอยู่ เพิ่มTesting.overrides.AbstractComponentคลาสapplication.jsในrequiresอาร์เรย์ของไฟล์ของคุณ

/**
 * Overrides the Ext.AbstracComponent's onBoxReady
 * method to add custom data attributes to the
 * component's dom structure.
 *
 * @author Brian Wendt
 */
Ext.define('Testing.overrides.AbstractComponent', {
  override: 'Ext.AbstractComponent',


  onBoxReady: function () {
    var me = this,
      el = me.getEl();


    if (el && el.dom && me.itemId) {
      el.dom.setAttribute(me.testIdAttr || 'data-selenium-id', me.itemId);
    }


    me.callOverridden(arguments);
  }
});

วิธีนี้ช่วยให้นักพัฒนาสามารถนำตัวระบุที่สื่อความหมายกลับมาใช้ซ้ำภายในโค้ดของพวกเขาและเพื่อให้ตัวระบุเหล่านั้นพร้อมใช้งานทุกครั้งที่มีการแสดงเพจ ไม่ต้องค้นหาอีกต่อไปผ่านรหัสที่ไม่ได้อธิบายและสร้างขึ้นแบบไดนามิก


2

เรากำลังพัฒนากรอบการทดสอบที่ใช้ซีลีเนียมและพบปัญหากับ extjs (เนื่องจากเป็นการแสดงผลฝั่งไคลเอ็นต์) ฉันพบว่ามีประโยชน์ในการมองหาองค์ประกอบเมื่อ DOM พร้อม

public static boolean waitUntilDOMIsReady(WebDriver driver) {
    def maxSeconds = DEFAULT_WAIT_SECONDS * 10
    for (count in 1..maxSeconds) {
        Thread.sleep(100)
        def ready = isDOMReady(driver);
        if (ready) {
            break;
        }
    }
}

public static boolean isDOMReady(WebDriver driver){
    return driver.executeScript("return document.readyState");
}

1

สำหรับ UI ที่ซับซ้อนที่ไม่ใช่ HTML อย่างเป็นทางการ xPath เป็นสิ่งที่คุณวางใจได้เสมอ แต่มีความซับซ้อนเล็กน้อยเมื่อต้องใช้ UI ที่แตกต่างกันโดยใช้ ExtJ

คุณสามารถใช้ Firebug และ Firexpath เป็นส่วนขยายของ firefox เพื่อทดสอบ xpath ขององค์ประกอบบางอย่างและส่ง xpath แบบธรรมดาเป็นพารามิเตอร์ไปยังซีลีเนียม

ตัวอย่างเช่นในรหัส java:

String fullXpath = "xpath=//div[@id='mainDiv']//div[contains(@class,'x-grid-row')]//table/tbody/tr[1]/td[1]//button"

selenium.click(fullXpath);

1

เมื่อฉันทดสอบแอปพลิเคชัน ExtJS โดยใช้ WebDriver ฉันใช้แนวทางถัดไป: ฉันมองหาฟิลด์ตามข้อความของฉลากและได้รับ@forแอตทริบิวต์จากฉลาก ตัวอย่างเช่นเรามีป้ายกำกับ

<label id="dynamic_id_label" class="TextboxLabel" for="textField_which_I_am_lloking_for">
Name Of Needed Label
<label/>

และเราจำเป็นต้องชี้ให้ WebDriver ป้อนข้อมูลบางอย่าง: //input[@id=(//label[contains(text(),'Name Of Needed Label')]/@for)].

ดังนั้นมันจะเลือก id จาก@forแอตทริบิวต์และใช้ต่อไป นี่อาจเป็นกรณีที่ง่ายที่สุด แต่ให้วิธีค้นหาองค์ประกอบ มันยากกว่ามากเมื่อคุณไม่มีป้ายกำกับ แต่คุณต้องหาองค์ประกอบบางอย่างและเขียน xpath ของคุณเพื่อมองหาพี่น้ององค์ประกอบลง / ขึ้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.