Puppetaria: สคริปต์ Puppeteer ที่เน้นการเข้าถึงเป็นหลัก

Johan Bay
Johan Bay

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 โดยตรง

CDP

ซึ่งทำให้เรามี 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 แบบที่แตกต่างกัน โดยเปลี่ยนขนาดหน้าเว็บและเปิดใช้การแคชองค์ประกอบการช่วยเหลือพิเศษหรือไม่

การเปรียบเทียบ: รันไทม์ทั้งหมดของการค้นหาองค์ประกอบ 4 รายการ 1,000 ครั้ง

เห็นได้ชัดว่ากลไกการค้นหาที่ CDP รองรับมีประสิทธิภาพสูงกว่ากลไกการค้นหาอีก 2 รายการที่ติดตั้งใช้งานใน Puppeteer เพียงอย่างเดียว และความแตกต่างสัมพัทธ์ดูเหมือนจะเพิ่มขึ้นอย่างมากตามขนาดหน้าเว็บ เราพบว่าการเรียกใช้การแยกย่อย DOM ของ JS ตอบสนองได้ดีกับการเปิดใช้การแคชการช่วยเหลือพิเศษ เมื่อปิดใช้การแคช ระบบจะคำนวณต้นไม้การช่วยเหลือพิเศษตามคำขอและทิ้งต้นไม้หลังจากการโต้ตอบแต่ละครั้งหากปิดใช้โดเมน การเปิดใช้โดเมนจะทำให้ Chromium แคชต้นไม้ที่คำนวณแล้วแทน

สําหรับการเรียกดู DOM ของ JS เราจะขอชื่อและบทบาทที่เข้าถึงได้ขององค์ประกอบทุกรายการในระหว่างการเรียกดู ดังนั้นหากปิดใช้การแคช Chromium จะคํานวณและทิ้งต้นไม้การช่วยเหลือพิเศษสําหรับองค์ประกอบทุกรายการที่เราเข้าชม ในทางกลับกัน สําหรับแนวทางที่อิงตาม CDP ระบบจะทิ้งต้นไม้ระหว่างการเรียก CDP แต่ละครั้งเท่านั้น เช่น สําหรับการค้นหาแต่ละครั้ง แนวทางเหล่านี้ยังได้รับประโยชน์จากการเปิดใช้การแคชด้วย เนื่องจากระบบจะเก็บรักษาโครงสร้างการช่วยเหลือพิเศษไว้ในการเรียกใช้ CDP แต่การปรับปรุงประสิทธิภาพจึงน้อยกว่าเมื่อเทียบกับวิธีอื่นๆ

แม้ว่าการเปิดใช้การแคชจะดูเหมาะกับการใช้งานในส่วนนี้ แต่ก็ยังมีค่าใช้จ่ายในการใช้หน่วยความจำเพิ่มเติม สำหรับสคริปต์ Puppeteer เช่น บันทึกไฟล์การติดตาม อาจทำให้เกิดปัญหาได้ เราจึงตัดสินใจที่จะไม่เปิดใช้การแคชต้นไม้การช่วยเหลือพิเศษโดยค่าเริ่มต้น ผู้ใช้สามารถเปิดใช้การแคชด้วยตนเองได้โดยเปิดใช้โดเมนการช่วยเหลือพิเศษของ CDP

เกณฑ์การเปรียบเทียบชุดทดสอบของเครื่องมือสําหรับนักพัฒนาซอฟต์แวร์

การเปรียบเทียบก่อนหน้านี้แสดงให้เห็นว่าการใช้กลไกการค้นหาของเราที่เลเยอร์ CDP ช่วยเพิ่มประสิทธิภาพในสถานการณ์การทดสอบหน่วยแบบคลินิก

ในการดูว่ามีความแตกต่างชัดเจนพอที่จะสังเกตได้ในสถานการณ์ที่สมจริงมากขึ้นในการใช้งานชุดทดสอบเต็มรูปแบบหรือไม่ เราได้แพตช์ชุดทดสอบแบบครบวงจรในเครื่องมือสำหรับนักพัฒนาเว็บเพื่อใช้ประโยชน์จากต้นแบบที่ใช้ JavaScript และ CDP และเปรียบเทียบรันไทม์ ในการเปรียบเทียบนี้ เราได้เปลี่ยนตัวเลือกทั้งหมด 43 รายการจาก [aria-label=…] เป็นตัวแฮนเดิลการค้นหาที่กำหนดเอง aria/… จากนั้นจึงติดตั้งใช้งานโดยใช้โปรโตไทป์แต่ละรายการ

โปรแกรมเลือกบางรายการใช้ในสคริปต์ทดสอบหลายครั้ง จํานวนจริงของการดำเนินการของตัวแฮนเดิลการค้นหา aria คือ 113 ครั้งต่อการเรียกใช้ชุด จํานวนการเลือกคําค้นหาทั้งหมดคือ 2253 รายการ ดังนั้นการเลือกคําค้นหาเพียงส่วนน้อยเท่านั้นที่เกิดขึ้นผ่านโปรโตไทป์

การเปรียบเทียบ: ชุดทดสอบ e2e

ดังที่เห็นในรูปภาพด้านบน รันไทม์ทั้งหมดแตกต่างกันอย่างชัดเจน ข้อมูลมีความผันผวนสูงเกินกว่าที่จะสรุปอะไรได้อย่างชัดเจน แต่เห็นได้ชัดว่าช่องว่างด้านประสิทธิภาพระหว่างโปรโตไทป์ 2 รายการแสดงในสถานการณ์นี้ด้วย

ปลายทาง CDP ใหม่

จากข้อมูลการเปรียบเทียบข้างต้น และเนื่องจากแนวทางที่อิงตาม Flag การเปิดตัวนั้นไม่เหมาะสมโดยทั่วไป เราจึงตัดสินใจที่จะติดตั้งใช้งานคําสั่ง CDP ใหม่สําหรับการค้นหาต้นไม้การช่วยเหลือพิเศษ ตอนนี้เราต้องหาวิธีสร้างอินเทอร์เฟซของปลายทางใหม่นี้

สำหรับกรณีการใช้งานของเราใน Puppeteer เราต้องใช้ปลายทางที่ใช้สิ่งที่เรียกกันว่า RemoteObjectIds เป็นอาร์กิวเมนต์ และหากต้องการให้เราค้นหาองค์ประกอบ DOM ที่เกี่ยวข้องได้ในภายหลัง ควรแสดงรายการออบเจ็กต์ที่มี backendNodeIds สำหรับองค์ประกอบ DOM

ดังที่เห็นในแผนภูมิด้านล่าง เราได้ลองใช้แนวทางต่างๆ มากมายเพื่อตอบสนองต่ออินเทอร์เฟซนี้ จากข้อมูลนี้ เราพบว่าขนาดของออบเจ็กต์ที่แสดงผล เช่น การแสดงผลโหนดการช่วยเหลือพิเศษทั้งหมดหรือเฉพาะ backendNodeIds นั้นไม่มีความแตกต่างที่สังเกตได้ ในทางกลับกัน เราพบว่าการใช้ NextInPreOrderIncludingIgnored ที่มีอยู่เป็นทางเลือกที่ไม่เหมาะสมในการใช้ตรรกะการวนซ้ำที่นี่ เนื่องจากทำให้ระบบทำงานช้าลงอย่างเห็นได้ชัด

การเปรียบเทียบการเปรียบเทียบการเปรียบเทียบการเรียกใช้ AXTree บน CDP

สรุป

เมื่อติดตั้งใช้งานปลายทาง CDP แล้ว เราได้ใช้ตัวแฮนเดิลการค้นหาในฝั่ง Puppeteer งานหลักๆ คือการปรับเปลี่ยนโครงสร้างโค้ดการจัดการการค้นหาเพื่อให้การค้นหาแก้ไขได้โดยตรงผ่าน CDP แทนการค้นหาผ่าน JavaScript ที่ประเมินในบริบทหน้าเว็บ

ขั้นตอนถัดไปคือ

แฮนเดิล aria ใหม่มาพร้อมกับ Puppeteer v5.4.0 เป็นแฮนเดิลการค้นหาในตัว เราหวังว่าจะเห็นผู้ใช้นำเครื่องมือนี้ไปใช้กับสคริปต์ทดสอบ และอยากฟังแนวคิดของคุณเกี่ยวกับวิธีที่เราจะทำให้เครื่องมือนี้มีประโยชน์มากยิ่งขึ้น

ดาวน์โหลดแชแนลตัวอย่าง

ลองใช้ Chrome Canary, Dev หรือ เบต้า เป็นเบราว์เซอร์สำหรับนักพัฒนาซอฟต์แวร์เริ่มต้น ช่องทางเวอร์ชันตัวอย่างเหล่านี้จะช่วยให้คุณเข้าถึงฟีเจอร์ล่าสุดของ DevTools, ทดสอบ API ของแพลตฟอร์มเว็บที่ล้ำสมัย และช่วยคุณค้นหาปัญหาในเว็บไซต์ได้ก่อนที่ผู้ใช้จะพบ

ติดต่อทีมเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome

ใช้ตัวเลือกต่อไปนี้เพื่อพูดคุยเกี่ยวกับฟีเจอร์ใหม่ การอัปเดต หรือสิ่งอื่นๆ ที่เกี่ยวข้องกับเครื่องมือสำหรับนักพัฒนาเว็บ