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

Johan Bay
Johan Bay

Puppeteer และแนวทางการใช้ตัวเลือก

Puppeteer เป็นไลบรารีการทำงานอัตโนมัติของเบราว์เซอร์สําหรับ Node ซึ่งช่วยให้คุณควบคุมเบราว์เซอร์ได้โดยใช้ 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 ข้อมูลโค้ด 1 รายการจากชุดทดสอบอาจมีลักษณะดังนี้

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 จะจัดการกับการกำหนดเส้นทางคำสั่งไปยังตำแหน่งที่ถูกต้อง

การดำเนินการของ Puppeteer เช่น การค้นหา การคลิก และการประเมินนิพจน์จะดำเนินการโดยใช้ประโยชน์จากคําสั่ง 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 Flag การเปิดตัว เพื่อดึงข้อมูลชื่อและบทบาทขององค์ประกอบแต่ละรายการในระหว่างการเรียกดู

การสํารวจ 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 ใหม่สําหรับการค้นหาต้นไม้การช่วยเหลือพิเศษ ตอนนี้เราต้องหาวิธีสร้างอินเทอร์เฟซของปลายทางใหม่นี้

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

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

การเปรียบเทียบ: การเปรียบเทียบต้นแบบการข้ามผ่าน AXTree ที่ใช้ CDP

สรุป

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

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

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

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

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

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

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