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

Johan Bay
Johan Bay

คนเชิดหุ่นกระบอกและวิธีเสนอตัวเลือก

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 อยู่ 2-3 วิธี หากต้องการทราบเหตุผล เรามาดูวิธีที่ Puppeteer ควบคุมเบราว์เซอร์กันก่อน

เบราว์เซอร์แสดงอินเทอร์เฟซการแก้ไขข้อบกพร่องผ่านโปรโตคอลที่เรียกว่า Chrome DevTools Protocol (CDP) ซึ่งจะแสดงฟังก์ชันการทำงาน เช่น "โหลดหน้าเว็บซ้ำ" หรือ "เรียกใช้ JavaScript นี้ในหน้าเว็บและส่งผลลัพธ์กลับมา" ผ่านอินเทอร์เฟซที่เข้าใจได้โดยไม่ต้องปรับภาษา

ทั้งฟรอนท์เอนด์ของเครื่องมือสำหรับนักพัฒนาเว็บและ Puppeteer ต่างก็ใช้ CDP เพื่อสื่อสารกับเบราว์เซอร์ ในการใช้คำสั่ง CDP จะต้องมีโครงสร้างพื้นฐานของเครื่องมือสำหรับนักพัฒนาเว็บอยู่ในคอมโพเนนต์ทั้งหมดของ Chrome เช่น ในเบราว์เซอร์ ในโหมดแสดงภาพ และอื่นๆ CDP จะดูแลการกำหนดเส้นทางคำสั่งไปยังที่ที่ถูกต้อง

การทำงานของการเชิดหุ่น เช่น การค้นหา การคลิก และการประเมินนิพจน์จะทำได้โดยใช้ประโยชน์จากคำสั่ง CDP เช่น Runtime.evaluate ซึ่งจะประเมิน JavaScript โดยตรงในบริบทหน้าเว็บและส่งผลลัพธ์กลับไป การดำเนินการอื่นๆ ของ Puppeteer เช่น การจำลองภาวะบกพร่องในการมองเห็นสี การถ่ายภาพหน้าจอ หรือการจับภาพการติดตามจะใช้ CDP เพื่อสื่อสารกับกระบวนการแสดงผล Blink โดยตรง

ซีดีพี

ซึ่งทำให้เรามี 2 เส้นทางในการใช้ฟังก์ชันการค้นหา ได้แก่

  • เขียนตรรกะการค้นหาของเราใน JavaScript และแทรกสิ่งนั้นลงในหน้าเว็บโดยใช้ Runtime.evaluate หรือ
  • ใช้ปลายทาง CDP ที่เข้าถึงและค้นหาแผนผังการช่วยเหลือพิเศษได้โดยตรงในกระบวนการ Blink

เราใช้ต้นแบบ 3 แบบ ดังนี้

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

การส่งผ่าน JS DOM

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

การส่งผ่านของ Puppeteer AXTree

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

การส่งผ่าน DOM ของ CDP

สำหรับต้นแบบนี้ เราใช้ปลายทาง CDP ใหม่เพื่อค้นหาโครงสร้างการช่วยเหลือพิเศษโดยเฉพาะ ด้วยวิธีนี้ การค้นหาอาจเกิดขึ้นในส่วนหลังผ่านการใช้งาน C++ แทนในบริบทของหน้าเว็บผ่าน JavaScript

การเปรียบเทียบการทดสอบหน่วย

รูปต่อไปนี้เปรียบเทียบรันไทม์ทั้งหมดของการค้นหาองค์ประกอบ 4 รายการ 1,000 ครั้งสำหรับต้นแบบทั้ง 3 ตัว เราได้ดำเนินการเปรียบเทียบในการกำหนดค่า 3 แบบที่แตกต่างกัน โดยขึ้นอยู่กับขนาดหน้าเว็บและเปิดใช้การแคชองค์ประกอบการช่วยเหลือพิเศษหรือไม่

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

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

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

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

การเปรียบเทียบชุดทดสอบของเครื่องมือสำหรับนักพัฒนาเว็บ

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

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

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

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

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

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

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

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

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

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

สรุปทุกอย่าง

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

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

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

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

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

ติดต่อทีม Chrome DevTools

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

  • ส่งข้อเสนอแนะหรือความคิดเห็นถึงเราทาง crbug.com
  • รายงานปัญหาเกี่ยวกับเครื่องมือสำหรับนักพัฒนาเว็บโดยใช้ตัวเลือกเพิ่มเติม   เพิ่มเติม   > ความช่วยเหลือ > รายงานปัญหาเกี่ยวกับเครื่องมือสำหรับนักพัฒนาเว็บในเครื่องมือสำหรับนักพัฒนาเว็บ
  • ทวีตที่ @ChromeDevTools
  • แสดงความคิดเห็นว่ามีอะไรใหม่ในวิดีโอ YouTube เครื่องมือสำหรับนักพัฒนาเว็บ หรือวิดีโอ YouTube สำหรับเครื่องมือสำหรับนักพัฒนาเว็บ