Puppeteer และแนวทางการใช้ตัวเลือก
Puppeteer เป็นไลบรารีการทำงานอัตโนมัติของเบราว์เซอร์สำหรับโหนด โดยให้คุณควบคุมเบราว์เซอร์โดยใช้ JavaScript API ที่ทันสมัยและไม่ซับซ้อน
แน่นอนว่างานที่สำคัญที่สุดของเบราว์เซอร์คือการท่องหน้าเว็บ การทำภารกิจนี้ให้เป็นแบบอัตโนมัติจึงเท่ากับการทําให้การโต้ตอบกับหน้าเว็บเป็นแบบอัตโนมัติ
ใน Puppeteer การดำเนินการนี้จะทำได้โดยการค้นหาองค์ประกอบ DOM โดยใช้ตัวเลือกที่อิงตามสตริงและดำเนินการต่างๆ เช่น การคลิกหรือพิมพ์ข้อความในองค์ประกอบ ตัวอย่างเช่น สคริปต์ที่เปิด developer.google.com, ค้นหาช่องค้นหา และค้นหา puppetaria
อาจมีลักษณะดังนี้
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://developers.google.com/', { waitUntil: 'load' });
// Find the search box using a suitable CSS selector.
const search = await page.$('devsite-search > form > div.devsite-search-container');
// Click to expand search box and focus it.
await search.click();
// Enter search string and press Enter.
await search.type('puppetaria');
await search.press('Enter');
})();
ดังนั้น วิธีที่ระบบระบุองค์ประกอบโดยใช้ตัวเลือกการค้นหาจึงเป็นองค์ประกอบที่บ่งบอกถึงประสบการณ์ของ Puppeteer ก่อนหน้านี้ ตัวเลือกใน Puppeteer ใช้ได้เฉพาะกับตัวเลือก CSS และ XPath ซึ่งแม้จะมีประสิทธิภาพมากในแง่การแสดงออก แต่ก็มีข้อเสียสำหรับการโต้ตอบกับเบราว์เซอร์แบบถาวรในสคริปต์
ตัวเลือกเชิงไวยากรณ์กับเชิงความหมาย
ตัวเลือก CSS มีลักษณะเป็นไวยากรณ์ โดยเชื่อมโยงกับการทำงานภายในของการแสดงผลแบบข้อความของต้นไม้ DOM อย่างใกล้ชิด ในแง่ที่อ้างอิงรหัสและชื่อคลาสจาก DOM ด้วยเหตุนี้ จึงมีเครื่องมือที่เป็นส่วนประกอบสำหรับนักพัฒนาเว็บเพื่อใช้แก้ไขหรือเพิ่มสไตล์ให้กับองค์ประกอบในหน้าเว็บ แต่นักพัฒนาซอฟต์แวร์จะมีสิทธิ์ควบคุมหน้าเว็บและโครงสร้าง DOM ของหน้าเว็บอย่างสมบูรณ์
ในทางกลับกัน สคริปต์ Puppeteer เป็นผู้สังเกตการณ์ภายนอกของหน้าเว็บ ดังนั้นเมื่อใช้ตัวเลือก CSS ในบริบทนี้ ตัวเลือกดังกล่าวจะนําเข้าสมมติฐานที่ซ่อนอยู่เกี่ยวกับวิธีติดตั้งใช้งานหน้าเว็บ ซึ่งสคริปต์ Puppeteer ไม่สามารถควบคุมได้
ผลที่ตามมาคือสคริปต์ดังกล่าวอาจทำงานไม่ถูกต้องและไวต่อการเปลี่ยนแปลงซอร์สโค้ด ตัวอย่างเช่น สมมติว่าผู้ใช้สคริปต์ Puppeteer สำหรับการทดสอบอัตโนมัติของเว็บแอปพลิเคชันที่มีโหนด <button>Submit</button>
เป็นองค์ประกอบย่อยลำดับที่ 3 ขององค์ประกอบ body
ข้อมูลโค้ดหนึ่งจากกรอบการทดสอบอาจมีลักษณะดังนี้
const button = await page.$('body:nth-child(3)'); // problematic selector
await button.click();
ในที่นี้ เราใช้ตัวเลือก 'body:nth-child(3)'
เพื่อค้นหาปุ่ม "ส่ง" แต่ตัวเลือกนี้เชื่อมโยงกับหน้าเว็บเวอร์ชันนี้อย่างแน่นหนา หากมีการเพิ่มองค์ประกอบเหนือปุ่มในภายหลัง ตัวเลือกนี้จะใช้งานไม่ได้อีกต่อไป
ข้อมูลนี้ไม่ใช่ข่าวใหม่สำหรับนักเขียนทดสอบ เนื่องจากผู้ใช้ Puppeteer พยายามเลือกตัวเลือกที่ทนทานต่อการเปลี่ยนแปลงดังกล่าวอยู่แล้ว Puppetaria เป็นเครื่องมือใหม่ที่เรามอบให้ผู้ใช้ในภารกิจนี้
ตอนนี้ Puppeteer มาพร้อมกับตัวแฮนเดิลการค้นหาทางเลือกที่อิงตามการค้นหาต้นไม้การช่วยเหลือพิเศษแทนที่จะใช้ตัวเลือก CSS แนวคิดพื้นฐานคือ หากองค์ประกอบที่เฉพาะเจาะจงซึ่งเราต้องการเลือกไม่เปลี่ยนแปลง โหนดการช่วยเหลือพิเศษที่เกี่ยวข้องก็ไม่ควรจะเปลี่ยนแปลงเช่นกัน
เราตั้งชื่อตัวเลือกดังกล่าวว่า "ตัวเลือก ARIA" และรองรับการค้นหาชื่อและบทบาทที่เข้าถึงได้ซึ่งคำนวณแล้วของต้นไม้การช่วยเหลือพิเศษ พร็อพเพอร์ตี้เหล่านี้มีลักษณะเป็นความหมายเมื่อเทียบกับตัวเลือก CSS องค์ประกอบเหล่านี้ไม่ได้เชื่อมโยงกับพร็อพเพอร์ตี้ทางไวยากรณ์ของ DOM แต่เป็นข้อบ่งชี้สำหรับลักษณะที่หน้าเว็บปรากฏผ่านเทคโนโลยีความช่วยเหลือพิเศษ เช่น โปรแกรมอ่านหน้าจอ
ในตัวอย่างสคริปต์ทดสอบด้านบน เราอาจใช้ตัวเลือก aria/Submit[role="button"]
เพื่อเลือกปุ่มที่ต้องการแทน โดยที่ Submit
หมายถึงชื่อที่เข้าถึงได้ขององค์ประกอบ
const button = await page.$('aria/Submit[role="button"]');
await button.click();
ตอนนี้ หากเราตัดสินใจที่จะเปลี่ยนเนื้อหาข้อความของปุ่มจาก Submit
เป็น Done
การทดสอบจะล้มเหลวอีกครั้ง แต่ในกรณีนี้เป็นสิ่งที่เราต้องการ เนื่องจากการเปลี่ยนชื่อปุ่มเป็นการเปลี่ยนเนื้อหาของหน้าเว็บ ซึ่งต่างจากการแสดงภาพหรือโครงสร้างใน DOM การทดสอบควรแจ้งเตือนเราเกี่ยวกับการเปลี่ยนแปลงดังกล่าวเพื่อให้แน่ใจว่าการเปลี่ยนแปลงดังกล่าวเป็นการเปลี่ยนแปลงโดยเจตนา
กลับไปที่ตัวอย่างขนาดใหญ่ที่มีแถบค้นหา เราอาจใช้ประโยชน์จากตัวแฮนเดิล aria
ใหม่และแทนที่
const search = await page.$('devsite-search > form > div.devsite-search-container');
กับ
const search = await page.$('aria/Open search[role="button"]');
เพื่อค้นหาแถบค้นหา
โดยทั่วไปแล้ว เราเชื่อว่าการใช้ตัวเลือก ARIA ดังกล่าวจะมีประโยชน์ต่อผู้ใช้ Puppeteer ดังนี้
- ทําให้ผู้เลือกในสคริปต์ทดสอบมีความยืดหยุ่นต่อการเปลี่ยนแปลงซอร์สโค้ดมากขึ้น
- ทําให้สคริปต์ทดสอบอ่านง่ายขึ้น (ชื่อที่เข้าถึงได้คือคําอธิบายเชิงความหมาย)
- กระตุ้นให้ใช้แนวทางปฏิบัติแนะนำในการกำหนดพร็อพเพอร์ตี้การช่วยเหลือพิเศษให้กับองค์ประกอบ
ส่วนที่เหลือของบทความนี้จะเจาะลึกรายละเอียดเกี่ยวกับวิธีที่เราติดตั้งใช้งานโปรเจ็กต์ Puppetaria
กระบวนการออกแบบ
ข้อมูลเบื้องต้น
ดังที่ได้กล่าวไปข้างต้น เราต้องการเปิดใช้องค์ประกอบการค้นหาตามชื่อและบทบาทที่เข้าถึงได้ พร็อพเพอร์ตี้เหล่านี้เป็นพร็อพเพอร์ตี้ของต้นไม้การช่วยเหลือพิเศษ ซึ่งเป็นคู่ของต้นไม้ DOM ปกติที่อุปกรณ์ต่างๆ เช่น โปรแกรมอ่านหน้าจอ ใช้เพื่อแสดงหน้าเว็บ
จากการศึกษาข้อกําหนดของการคํานวณชื่อที่เข้าถึงได้ เป็นที่แน่ชัดว่าการคํานวณชื่อขององค์ประกอบนั้นไม่ใช่เรื่องง่าย ตั้งแต่ต้นเราจึงตัดสินใจว่าต้องการนําโครงสร้างพื้นฐานที่มีอยู่ของ Chromium มาใช้ใหม่สําหรับงานนี้
แนวทางที่เราใช้ในการติดตั้งใช้งาน
แม้ว่าเราจะจำกัดตัวเองให้ใช้ต้นไม้การช่วยเหลือพิเศษของ Chromium เท่านั้น แต่ก็มีวิธีต่างๆ มากมายที่เราสามารถใช้การค้นหา ARIA ใน Puppeteer มาดูสาเหตุกันก่อนว่า Puppeteer ควบคุมเบราว์เซอร์อย่างไร
โดยเบราว์เซอร์จะแสดงอินเทอร์เฟซการแก้ไขข้อบกพร่องผ่านโปรโตคอลที่เรียกว่า Chrome DevTools Protocol (CDP) ซึ่งจะแสดงฟังก์ชันการทำงานต่างๆ เช่น "โหลดหน้าเว็บซ้ำ" หรือ "เรียกใช้ JavaScript นี้ในหน้าเว็บและแสดงผลลัพธ์กลับ" ผ่านอินเทอร์เฟซที่ไม่ขึ้นอยู่กับภาษา
ทั้งส่วนหน้าของ DevTools และ Puppeteer ใช้ CDP เพื่อสื่อสารกับเบราว์เซอร์ ในการใช้คำสั่ง CDP จะต้องมีโครงสร้างพื้นฐานของเครื่องมือสำหรับนักพัฒนาเว็บอยู่ในคอมโพเนนต์ทั้งหมดของ Chrome เช่น ในเบราว์เซอร์ ในโหมดแสดงภาพ และอื่นๆ CDP จะจัดการกับการกำหนดเส้นทางคำสั่งไปยังตำแหน่งที่ถูกต้อง
การทำงานของโปรแกรมเชิดหุ่น เช่น การค้นหา การคลิก และการประเมินนิพจน์จะทำได้โดยใช้ประโยชน์จากคำสั่ง CDP เช่น Runtime.evaluate
ซึ่งจะประเมิน JavaScript โดยตรงในบริบทหน้าเว็บและส่งผลลัพธ์กลับไป การดำเนินการอื่นๆ ของ Puppeteer เช่น การจําลองการมองเห็นสีที่บกพร่อง การจับภาพหน้าจอ หรือการบันทึกร่องรอยจะใช้ CDP เพื่อสื่อสารกับกระบวนการแสดงผลของ Blink โดยตรง
ซึ่งทำให้เรามี 2 เส้นทางในการใช้ฟังก์ชันการค้นหา ได้แก่
- เขียนตรรกะการค้นหาของเราใน JavaScript และแทรกสิ่งนั้นลงในหน้าเว็บโดยใช้
Runtime.evaluate
หรือ - ใช้ปลายทาง CDP ที่เข้าถึงและค้นหาต้นไม้การช่วยเหลือพิเศษได้โดยตรงในกระบวนการ Blink
เราใช้ต้นแบบ 3 แบบ ดังนี้
- การเรียกใช้ JS DOM - อิงตามการแทรก JavaScript ลงในหน้า
- การเรียกใช้ AXTree ของ Puppeteer - อิงตามการใช้การเข้าถึง CDP ที่มีอยู่ไปยังต้นไม้การช่วยเหลือพิเศษ
- การเรียกใช้ DOM ของ CDP - การใช้ปลายทาง CDP ใหม่ที่สร้างมาเพื่อค้นหาต้นไม้การช่วยเหลือพิเศษโดยเฉพาะ
การสํารวจ DOM ของ JS
ต้นแบบนี้จะข้ามผ่าน DOM โดยสมบูรณ์และใช้ element.computedName
และ element.computedRole
ซึ่งกั้นไว้ในแฟล็กการเปิดตัว ComputedAccessibilityInfo
เพื่อดึงข้อมูลชื่อและบทบาทของแต่ละองค์ประกอบระหว่างการข้ามผ่าน
การสํารวจ AXTree ของ Puppeteer
ในส่วนนี้ เราจะดึงข้อมูลแผนผังการช่วยเหลือพิเศษทั้งหมดผ่าน CDP และเรียกดูใน Puppeteer จากนั้นระบบจะแมปโหนดการช่วยเหลือพิเศษที่ได้มากับโหนด DOM
การสํารวจ DOM ของ CDP
สำหรับต้นแบบนี้ เราใช้ปลายทาง CDP ใหม่เพื่อค้นหาโครงสร้างการช่วยเหลือพิเศษโดยเฉพาะ วิธีนี้ช่วยให้การค้นหาเกิดขึ้นที่แบ็กเอนด์ผ่านการติดตั้งใช้งาน C++ แทนที่จะเป็นในบริบทหน้าเว็บผ่าน JavaScript
การเปรียบเทียบการทดสอบหน่วย
รูปต่อไปนี้เปรียบเทียบรันไทม์ทั้งหมดของการค้นหาองค์ประกอบ 4 รายการ 1,000 ครั้งสำหรับต้นแบบทั้ง 3 ตัว การทดสอบประสิทธิภาพนี้ดำเนินการในการกําหนดค่า 3 แบบที่แตกต่างกัน โดยเปลี่ยนขนาดหน้าเว็บและเปิดใช้การแคชองค์ประกอบการช่วยเหลือพิเศษหรือไม่
เห็นได้ชัดว่ากลไกการค้นหาที่ CDP รองรับมีประสิทธิภาพสูงกว่ากลไกการค้นหาอีก 2 รายการที่ติดตั้งใช้งานใน Puppeteer เพียงอย่างเดียว และความแตกต่างสัมพัทธ์ดูเหมือนจะเพิ่มขึ้นอย่างมากตามขนาดหน้าเว็บ เราพบว่าการเรียกใช้การแยกย่อย DOM ของ JS ตอบสนองได้ดีกับการเปิดใช้การแคชการช่วยเหลือพิเศษ เมื่อปิดใช้การแคช ระบบจะคำนวณต้นไม้การช่วยเหลือพิเศษตามคำขอและทิ้งต้นไม้หลังจากการโต้ตอบแต่ละครั้งหากปิดใช้โดเมน การเปิดใช้โดเมนจะทำให้ Chromium แคชต้นไม้ที่คำนวณแล้วแทน
สําหรับการเรียกดู DOM ของ JS เราจะขอชื่อและบทบาทที่เข้าถึงได้ขององค์ประกอบทุกรายการในระหว่างการเรียกดู ดังนั้นหากปิดใช้การแคช Chromium จะคํานวณและทิ้งต้นไม้การช่วยเหลือพิเศษสําหรับองค์ประกอบทุกรายการที่เราเข้าชม ในทางกลับกัน สําหรับแนวทางที่อิงตาม CDP ระบบจะทิ้งต้นไม้ระหว่างการเรียก CDP แต่ละครั้งเท่านั้น เช่น สําหรับการค้นหาแต่ละครั้ง แนวทางเหล่านี้ยังได้รับประโยชน์จากการเปิดใช้การแคชด้วย เนื่องจากระบบจะเก็บรักษาโครงสร้างการช่วยเหลือพิเศษไว้ในการเรียกใช้ CDP แต่การปรับปรุงประสิทธิภาพจึงน้อยกว่าเมื่อเทียบกับวิธีอื่นๆ
แม้ว่าการเปิดใช้การแคชจะดูเหมาะกับการใช้งานในส่วนนี้ แต่ก็ยังมีค่าใช้จ่ายในการใช้หน่วยความจำเพิ่มเติม สำหรับสคริปต์ Puppeteer เช่น บันทึกไฟล์การติดตาม อาจทำให้เกิดปัญหาได้ เราจึงตัดสินใจที่จะไม่เปิดใช้การแคชต้นไม้การช่วยเหลือพิเศษโดยค่าเริ่มต้น ผู้ใช้สามารถเปิดใช้การแคชด้วยตนเองได้โดยเปิดใช้โดเมนการช่วยเหลือพิเศษของ CDP
เกณฑ์การเปรียบเทียบชุดทดสอบของเครื่องมือสําหรับนักพัฒนาซอฟต์แวร์
การเปรียบเทียบก่อนหน้านี้แสดงให้เห็นว่าการใช้กลไกการค้นหาของเราที่เลเยอร์ CDP ช่วยเพิ่มประสิทธิภาพในสถานการณ์การทดสอบหน่วยแบบคลินิก
ในการดูว่ามีความแตกต่างชัดเจนพอที่จะสังเกตได้ในสถานการณ์ที่สมจริงมากขึ้นในการใช้งานชุดทดสอบเต็มรูปแบบหรือไม่ เราได้แพตช์ชุดทดสอบแบบครบวงจรในเครื่องมือสำหรับนักพัฒนาเว็บเพื่อใช้ประโยชน์จากต้นแบบที่ใช้ JavaScript และ CDP และเปรียบเทียบรันไทม์ ในการเปรียบเทียบนี้ เราได้เปลี่ยนตัวเลือกทั้งหมด 43 รายการจาก [aria-label=…]
เป็นตัวแฮนเดิลการค้นหาที่กำหนดเอง aria/…
จากนั้นจึงติดตั้งใช้งานโดยใช้โปรโตไทป์แต่ละรายการ
โปรแกรมเลือกบางรายการใช้ในสคริปต์ทดสอบหลายครั้ง จํานวนจริงของการดำเนินการของตัวแฮนเดิลการค้นหา aria
คือ 113 ครั้งต่อการเรียกใช้ชุด จํานวนการเลือกคําค้นหาทั้งหมดคือ 2253 รายการ ดังนั้นการเลือกคําค้นหาเพียงส่วนน้อยเท่านั้นที่เกิดขึ้นผ่านโปรโตไทป์
ดังที่เห็นในรูปภาพด้านบน รันไทม์ทั้งหมดแตกต่างกันอย่างชัดเจน ข้อมูลมีความผันผวนสูงเกินกว่าที่จะสรุปอะไรได้อย่างชัดเจน แต่เห็นได้ชัดว่าช่องว่างด้านประสิทธิภาพระหว่างโปรโตไทป์ 2 รายการแสดงในสถานการณ์นี้ด้วย
ปลายทาง CDP ใหม่
จากข้อมูลการเปรียบเทียบข้างต้น และเนื่องจากแนวทางที่อิงตาม Flag การเปิดตัวนั้นไม่เหมาะสมโดยทั่วไป เราจึงตัดสินใจที่จะติดตั้งใช้งานคําสั่ง CDP ใหม่สําหรับการค้นหาต้นไม้การช่วยเหลือพิเศษ ตอนนี้เราต้องหาวิธีสร้างอินเทอร์เฟซของปลายทางใหม่นี้
สำหรับกรณีการใช้งานของเราใน Puppeteer เราต้องใช้ปลายทางที่ใช้สิ่งที่เรียกกันว่า RemoteObjectIds
เป็นอาร์กิวเมนต์ และหากต้องการให้เราค้นหาองค์ประกอบ DOM ที่เกี่ยวข้องได้ในภายหลัง ควรแสดงรายการออบเจ็กต์ที่มี backendNodeIds
สำหรับองค์ประกอบ DOM
ดังที่เห็นในแผนภูมิด้านล่าง เราได้ลองใช้แนวทางต่างๆ มากมายเพื่อตอบสนองต่ออินเทอร์เฟซนี้ จากข้อมูลนี้ เราพบว่าขนาดของออบเจ็กต์ที่แสดงผล เช่น การแสดงผลโหนดการช่วยเหลือพิเศษทั้งหมดหรือเฉพาะ backendNodeIds
นั้นไม่มีความแตกต่างที่สังเกตได้ ในทางกลับกัน เราพบว่าการใช้ NextInPreOrderIncludingIgnored
ที่มีอยู่เป็นทางเลือกที่ไม่เหมาะสมในการใช้ตรรกะการวนซ้ำที่นี่ เนื่องจากทำให้ระบบทำงานช้าลงอย่างเห็นได้ชัด
สรุป
เมื่อติดตั้งใช้งานปลายทาง CDP แล้ว เราได้ใช้ตัวแฮนเดิลการค้นหาในฝั่ง Puppeteer งานหลักๆ คือการปรับเปลี่ยนโครงสร้างโค้ดการจัดการการค้นหาเพื่อให้การค้นหาแก้ไขได้โดยตรงผ่าน CDP แทนการค้นหาผ่าน JavaScript ที่ประเมินในบริบทหน้าเว็บ
ขั้นตอนถัดไปคือ
แฮนเดิล aria
ใหม่มาพร้อมกับ Puppeteer v5.4.0 เป็นแฮนเดิลการค้นหาในตัว เราหวังว่าจะเห็นผู้ใช้นำเครื่องมือนี้ไปใช้กับสคริปต์ทดสอบ และอยากฟังแนวคิดของคุณเกี่ยวกับวิธีที่เราจะทำให้เครื่องมือนี้มีประโยชน์มากยิ่งขึ้น
ดาวน์โหลดแชแนลตัวอย่าง
ลองใช้ Chrome Canary, Dev หรือ เบต้า เป็นเบราว์เซอร์สำหรับนักพัฒนาซอฟต์แวร์เริ่มต้น ช่องทางเวอร์ชันตัวอย่างเหล่านี้จะช่วยให้คุณเข้าถึงฟีเจอร์ล่าสุดของ DevTools, ทดสอบ API ของแพลตฟอร์มเว็บที่ล้ำสมัย และช่วยคุณค้นหาปัญหาในเว็บไซต์ได้ก่อนที่ผู้ใช้จะพบ
ติดต่อทีมเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome
ใช้ตัวเลือกต่อไปนี้เพื่อพูดคุยเกี่ยวกับฟีเจอร์ใหม่ การอัปเดต หรือสิ่งอื่นๆ ที่เกี่ยวข้องกับเครื่องมือสำหรับนักพัฒนาเว็บ
- ส่งความคิดเห็นและคำขอฟีเจอร์ถึงเราได้ที่ crbug.com
- รายงานปัญหาเกี่ยวกับเครื่องมือสำหรับนักพัฒนาเว็บโดยใช้ตัวเลือกเพิ่มเติม > ความช่วยเหลือ > รายงานปัญหาเกี่ยวกับเครื่องมือสำหรับนักพัฒนาเว็บในเครื่องมือสำหรับนักพัฒนาเว็บ
- ทวีตที่ @ChromeDevTools
- แสดงความคิดเห็นในวิดีโอ YouTube เกี่ยวกับข่าวสารใน DevTools หรือวิดีโอ YouTube เกี่ยวกับเคล็ดลับใน DevTools