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> بهعنوان سومین فرزند عنصر 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 (CDP) نمایش میدهد. این کار عملکردهایی مانند «بار کردن مجدد صفحه» یا «اجرای این قطعه جاوا اسکریپت در صفحه و برگرداندن نتیجه» را ازطریق یک میانای مستقل از زبان نشان میدهد.
هم DevTools front-end و هم Puppeteer از CDP برای صحبت با مرورگر استفاده میکنند. برای پیادهسازی فرمانهای CDP، زیرساخت DevTools در داخل همه مؤلفههای Chrome وجود دارد: در مرورگر، در رندرکننده، و غیره. CDP مسیریابی فرمانها به مکان درست را برعهده دارد.
کنشهای Puppeteer مانند پرسمان، کلیک، و ارزیابی عبارتها بااستفاده از فرمانهای CDP مانند Runtime.evaluate انجام میشود که جاوا اسکریپت را مستقیماً در بافت صفحه ارزیابی میکند و نتیجه را برمیگرداند. سایر کنشهای Puppeteer مانند شبیهسازی نقص دید رنگ، گرفتن نماگرفت، یا ضبط ردپاها از CDP برای برقراری ارتباط مستقیم با فرایند پردازشگر Blink استفاده میکنند.

این امر دو مسیر برای پیادهسازی عملکرد پُرسمان ما باقی میگذارد؛ میتوانیم:
- منطق پُرسمان خود را در جاوا اسکریپت بنویسید و آن را بااستفاده از
Runtime.evaluateبه صفحه تزریق کنید، یا - از نقطه پایانی CDP استفاده کنید که بتواند مستقیماً در فرایند Blink به درخت دسترسپذیری دسترسی داشته باشد و آن را پرسمان کند.
ما ۳ نمونه اولیه را پیادهسازی کردیم:
- پیمایش JS DOM - براساس تزریق جاوا اسکریپت به صفحه
- Puppeteer AXTree traversal - based on using the existing CDP access to the accessibility tree
- پیمایش CDP DOM - بااستفاده از نقطه پایانی جدید CDP که بهطور خاص برای پرسمان درخت دسترسپذیری ساخته شده است
پیمایش DOM جاوااسکریپت
این نمونه اولیه پیمایش کامل DOM را انجام میدهد و از element.computedName و element.computedRole، که با پرچم راهاندازی ComputedAccessibilityInfo محدود شدهاند، برای بازیابی نام و نقش هر عنصر درطول پیمایش استفاده میکند.
Puppeteer AXTree traversal
در اینجا، درعوض درخت دسترسپذیری کامل را ازطریق CDP بازیابی میکنیم و آن را در Puppeteer پیمایش میکنیم. سپس گرههای دسترسپذیری حاصل به گرههای DOM نگاشت میشوند.
پیمایش DOM پروتکل Chrome DevTools
برای این نمونه اولیه، یک نقطه پایانی CDP جدید را بهطور خاص برای پرسمان درخت دسترسپذیری پیادهسازی کردیم. بهاینترتیب، پرسمان میتواند در پسزمینه ازطریق پیادهسازی C++ بهجای بافت صفحه ازطریق جاوا اسکریپت انجام شود.
محک آزمون واحد
شکل زیر کل زمان اجرای پُرسمان چهار عنصر را بهمدت ۱۰۰۰ بار برای ۳ نمونه اولیه مقایسه میکند. این محک در ۳ پیکربندی مختلف با اندازههای صفحه متفاوت و با فعال یا غیرفعال بودن ذخیره عناصر دسترسپذیری در حافظه نهان اجرا شد.
کاملاً واضح است که شکاف عملکرد قابلتوجهی بین سازوکار پُرسمان با پشتیبانی CDP و دو سازوکار دیگر که صرفاً در Puppeteer پیادهسازی شدهاند وجود دارد، و بهنظر میرسد تفاوت نسبی با اندازه صفحه بهطور چشمگیری افزایش مییابد. تا حدودی جالب است که میبینیم نمونه اولیه پیمایش JS DOM به فعال کردن حافظه پنهان دسترسیپذیری بهخوبی پاسخ میدهد. با غیرفعال شدن ذخیرهسازی در حافظه نهان، درخت دسترسپذیری براساس تقاضا محاسبه میشود و اگر دامنه غیرفعال باشد، درخت پساز هر تعامل دور انداخته میشود. فعال کردن دامنه باعث میشود Chromium بهجای آن، درخت محاسبهشده را در حافظه نهان ذخیره کند.
برای پیمایش JS DOM، نام و نقش قابلدسترس را برای هر عنصر درطول پیمایش درخواست میکنیم، بنابراین اگر ذخیرهسازی موقت غیرفعال باشد، Chromium درخت دسترسپذیری را برای هر عنصری که بازدید میکنیم محاسبه و حذف میکند. ازسوی دیگر، برای رویکردهای مبتنی بر CDP، درخت فقط بین هر تماس با CDP، یعنی برای هر پُرسمان، دور انداخته میشود. این رویکردها همچنین از فعال کردن ذخیرهسازی مؤقت بهره میبرند، زیرا درخت دسترسپذیری در تماسهای CDP حفظ میشود، اما افزایش عملکرد در مقایسه با آن کمتر است.
اگرچه فعال کردن ذخیرهسازی در حافظه نهان در اینجا مطلوب بهنظر میرسد، اما هزینه استفاده از حافظه اضافی را بههمراه دارد. برای اسکریپتهای Puppeteer که مثلاً فایلهای ردیابی را ضبط میکنند، این میتواند مشکلساز باشد. بنابراین تصمیم گرفتیم ذخیره موقت درخت دسترسپذیری را بهطور پیشفرض فعال نکنیم. کاربران میتوانند با فعال کردن دامنه دسترسپذیری CDP، حافظه پنهان را خودشان فعال کنند.
محک مجموعه آزمون DevTools
معیار قبلی نشان داد که پیادهسازی سازوکار پُرسمان ما در لایه CDP باعث افزایش عملکرد در سناریوی آزمایش واحد بالینی میشود.
برای اینکه ببینیم آیا این تفاوت بهاندازهای قابلتوجه است که در سناریو واقعیتر اجرای یک مجموعه آزمون کامل مشهود باشد، مجموعه آزمون سرتاسری DevTools را وصله کردیم تا از نمونههای اولیه مبتنی بر جاوا اسکریپت و CDP استفاده کنیم و زمانهای اجرا را مقایسه کردیم. در این محک، درمجموع ۴۳ انتخابگر را از [aria-label=…] به گرداننده پُرسمان سفارشی aria/… تغییر دادیم، که سپس آن را بااستفاده از هریک از نمونههای اولیه پیادهسازی کردیم.
برخیاز انتخابکنندهها چندینبار در اسکریپتهای آزمایشی استفاده میشوند، بنابراین تعداد واقعی اجراهای کنترلکننده پُرسمان aria، ۱۱۳ بار در هر اجرای مجموعه بود. تعداد کل انتخابهای پُرسمان ۲۲۵۳ بود، بنابراین فقط کسری از انتخابهای پُرسمان ازطریق نمونههای اولیه اتفاق افتاد.
همانطور که در شکل بالا دیده میشود، تفاوت قابلتشخیصی در کل زمان اجرا وجود دارد. دادهها بیشازحد نامفهوم است و نمیتوان نتیجهگیری خاصی کرد، اما واضح است که شکاف عملکرد بین دو نمونه اولیه در این سناریو نیز نشان داده میشود.
نقطه پایان CDP جدید
با توجه به معیارهای بالا، و ازآنجاییکه رویکرد مبتنی بر پرچم راهاندازی بهطور کلی نامطلوب بود، تصمیم گرفتیم با پیادهسازی یک فرمان CDP جدید برای پرسمان درخت دسترسپذیری پیش برویم. اکنون باید میانای این نقطه پایانی جدید را طراحی میکردیم.
برای مورد استفاده ما در Puppeteer، به نقطه پایانی نیاز داریم تا بهاصطلاح RemoteObjectIds را بهعنوان آرگومان درنظر بگیرد و برای اینکه بتوانیم عناصر DOM مربوطه را بعداز آن پیدا کنیم، باید فهرستی از اشیاء را برگرداند که حاوی backendNodeIds برای عناصر DOM باشد.
همانطور که در نمودار زیر دیده میشود، ما رویکردهای متعددی را برای برآورده کردن این میانای کاربری امتحان کردیم. از این طریق متوجه شدیم که اندازه اشیای برگرداندهشده، یعنی اینکه آیا گرههای دسترسپذیری کامل را برگرداندهایم یا فقط backendNodeIds را، تفاوت قابلتشخیصی ایجاد نمیکند. ازسوی دیگر، متوجه شدیم که استفاده از NextInPreOrderIncludingIgnored موجود انتخاب ضعیفی برای پیادهسازی منطق پیمایش در اینجا بود، زیرا باعث کندی قابلتوجهی شد.
درحال جمعبندی کردن
اکنون، با وجود نقطه پایانی CDP، کنترلکننده پُرسمان را در سمت Puppeteer پیادهسازی کردیم. بخش عمده کار در اینجا بازسازی کد مدیریت پُرسمان بود تا پُرسمانها بتوانند مستقیماً ازطریق CDP حلوفصل شوند، بهجای اینکه ازطریق جاوا اسکریپت ارزیابیشده در بافت صفحه پُرسمان شوند.
حالا چه میشود؟
گرداننده جدید aria با Puppeteer نسخه ۵.۴.۰ بهعنوان گرداننده پُرسمان داخلی عرضه شد. مشتاقانه منتظریم ببینیم کاربران چگونه آن را در اسکریپتهای آزمایشی خود بهکار میگیرند و بیصبرانه منتظر شنیدن ایدههای شما برای مفیدتر کردن آن هستیم!
بارگیری کانالهای پیشنمایش
بهتر است از Chrome Canary، Dev، یا Beta بهعنوان مرورگر توسعه پیشفرضتان استفاده کنید. این کانالهای پیشنمایش به شما امکان میدهند به جدیدترین ویژگیهای DevTools دسترسی داشته باشید، میاناهای برنامهسازی کاربردی پلاتفرم وب پیشرفته را آزمایش کنید، و به شما کمک میکنند مشکلات سایتتان را قبلاز اینکه کاربران با آنها مواجه شوند پیدا کنید!
تماس با تیم Chrome DevTools
از گزینههای زیر برای بحث درباره ویژگیهای جدید، بهروزرسانیها، یا هر چیز دیگری که به DevTools مربوط میشود استفاده کنید.
- بازخورد و درخواستهای ویژگی را در crbug.com برای ما ارسال کنید.
- بااستفاده از گزینههای بیشتر > راهنما > گزارش کردن مشکل DevTools در DevTools، مشکل DevTools را گزارش کنید.
- در @ChromeDevTools توییت کنید.
- در ویدیوهای YouTube «موارد جدید در DevTools» یا ویدیوهای YouTube «نکات DevTools» نظر بگذارید.