اختبار التراجع البصري
يكتشف اختبار التراجع البصري التغييرات البصرية غير المقصودة في تطبيقك من خلال مقارنة لقطات الشاشة بمرور الوقت. في هذا الدرس، سنستكشف تقنيات مقارنة لقطات الشاشة وأدوات مثل Percy وChromatic وخوارزميات الفرق البصري واستراتيجيات الحفاظ على الاتساق البصري عبر الإصدارات.
لماذا يهم اختبار التراجع البصري
يساعدك اختبار التراجع البصري على:
- اكتشاف تغييرات CSS غير المقصودة التي تكسر التخطيطات
- كشف اختلافات العرض عبر المتصفحات
- التحقق من التصميم المستجيب عبر أحجام الشاشات المختلفة
- منع الأخطاء البصرية من الوصول إلى الإنتاج
- الحفاظ على العلامة التجارية المتسقة وأنظمة التصميم
- أتمتة ضمان الجودة البصرية التي تتطلب تقليديًا فحصًا يدويًا
- توثيق التغييرات البصرية لمراجعة التصميم
مفهوم أساسي: يكمل اختبار التراجع البصري الاختبار الوظيفي. بينما تتحقق اختبارات الوحدة والتكامل من السلوك، تتحقق الاختبارات البصرية من المظهر. كلاهما ضروري لضمان الجودة الشامل.
كيف يعمل اختبار التراجع البصري
يتضمن سير العمل الأساسي أربع خطوات:
- إنشاء خط الأساس: التقاط لقطات شاشة لتطبيقك في حالة جيدة معروفة
- تنفيذ الاختبار: بعد تغييرات الكود، التقاط لقطات شاشة جديدة لنفس العروض
- المقارنة البصرية: مقارنة لقطات الشاشة الجديدة بيكسل ببيكسل مع خطوط الأساس
- المراجعة والموافقة: مراجعة الاختلافات والموافقة على التغييرات المقصودة أو رفض الأخطاء
اختبار لقطات الشاشة الأساسي باستخدام Puppeteer
ابدأ بالتقاط لقطات الشاشة البسيطة باستخدام Puppeteer (Node.js):
// visual-test.js
const puppeteer = require('puppeteer');
const fs = require('fs');
const pixelmatch = require('pixelmatch');
const { PNG } = require('pngjs');
async function captureScreenshot(url, outputPath) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// تعيين منفذ عرض متسق
await page.setViewport({ width: 1280, height: 720 });
await page.goto(url, { waitUntil: 'networkidle0' });
// انتظار تحميل الخطوط والصور
await page.evaluateHandle('document.fonts.ready');
await page.screenshot({
path: outputPath,
fullPage: true,
});
await browser.close();
}
async function compareScreenshots(baselinePath, currentPath, diffPath) {
const baseline = PNG.sync.read(fs.readFileSync(baselinePath));
const current = PNG.sync.read(fs.readFileSync(currentPath));
const { width, height } = baseline;
const diff = new PNG({ width, height });
const numDiffPixels = pixelmatch(
baseline.data,
current.data,
diff.data,
width,
height,
{ threshold: 0.1 }
);
fs.writeFileSync(diffPath, PNG.sync.write(diff));
return numDiffPixels;
}
// الاستخدام
(async () => {
const url = 'http://localhost:3000/';
// التقاط لقطة الشاشة الحالية
await captureScreenshot(url, './screenshots/current.png');
// المقارنة مع خط الأساس
if (fs.existsSync('./screenshots/baseline.png')) {
const diffPixels = await compareScreenshots(
'./screenshots/baseline.png',
'./screenshots/current.png',
'./screenshots/diff.png'
);
console.log(`وحدات البيكسل المختلفة: ${diffPixels}`);
if (diffPixels > 100) { // العتبة
console.error('تم اكتشاف تراجع بصري!');
process.exit(1);
}
} else {
// التشغيل الأول - إنشاء خط الأساس
fs.copyFileSync(
'./screenshots/current.png',
'./screenshots/baseline.png'
);
console.log('تم إنشاء خط الأساس');
}
})();
Laravel Dusk للاختبار البصري
استخدم Laravel Dusk للاختبار البصري المستند إلى المتصفح في تطبيقات PHP:
<?php
namespace Tests\Browser;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class VisualRegressionTest extends DuskTestCase
{
protected $screenshotPath = 'tests/Browser/screenshots';
protected $baselinePath = 'tests/Browser/baseline';
public function test_homepage_visual_regression()
{
$this->browse(function (Browser $browser) {
$browser->visit('/')
->waitFor('#app')
->resize(1280, 720);
// انتظار تحميل المحتوى الديناميكي
$browser->pause(1000);
$screenshotName = 'homepage';
$browser->screenshot($screenshotName);
$this->compareScreenshots($screenshotName);
});
}
public function test_product_page_visual_regression()
{
$this->browse(function (Browser $browser) {
$product = Product::factory()->create();
$browser->visit("/products/{$product->id}")
->waitFor('.product-details')
->resize(1280, 720)
->screenshot('product-page');
$this->compareScreenshots('product-page');
});
}
public function test_responsive_layouts()
{
$this->browse(function (Browser $browser) {
$viewports = [
['mobile', 375, 667],
['tablet', 768, 1024],
['desktop', 1920, 1080],
];
foreach ($viewports as [$name, $width, $height]) {
$browser->visit('/')
->resize($width, $height)
->pause(500)
->screenshot("homepage-{$name}");
$this->compareScreenshots("homepage-{$name}");
}
});
}
protected function compareScreenshots($name)
{
$currentPath = "{$this->screenshotPath}/{$name}.png";
$baselinePath = "{$this->baselinePath}/{$name}.png";
$diffPath = "{$this->screenshotPath}/{$name}-diff.png";
if (!file_exists($baselinePath)) {
// إنشاء خط الأساس في التشغيل الأول
copy($currentPath, $baselinePath);
$this->markTestSkipped('تم إنشاء خط الأساس لـ ' . $name);
return;
}
// استخدام ImageMagick للمقارنة
exec(
"compare -metric AE {$baselinePath} {$currentPath} {$diffPath} 2>&1",
$output,
$returnCode
);
$diffPixels = (int) $output[0];
// السماح بالاختلافات الصغيرة (مكافحة التعرج، اختلافات العرض)
$threshold = 100;
$this->assertLessThan(
$threshold,
$diffPixels,
"تم اكتشاف تراجع بصري: {$diffPixels} بيكسل مختلف"
);
}
}
Percy للاختبار البصري الآلي
Percy هي منصة اختبار بصري تجارية مع تكامل ممتاز مع CI/CD:
# تثبيت Percy CLI
npm install --save-dev @percy/cli @percy/puppeteer
# percy-test.js
const puppeteer = require('puppeteer');
const percySnapshot = require('@percy/puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// الصفحة الرئيسية
await page.goto('http://localhost:3000/');
await percySnapshot(page, 'Homepage');
// صفحة المنتجات
await page.goto('http://localhost:3000/products');
await percySnapshot(page, 'Products Page');
// تفاصيل المنتج
await page.goto('http://localhost:3000/products/1');
await percySnapshot(page, 'Product Detail');
// لقطات مستجيبة
await page.setViewport({ width: 375, height: 667 });
await page.goto('http://localhost:3000/');
await percySnapshot(page, 'Homepage Mobile');
await page.setViewport({ width: 768, height: 1024 });
await percySnapshot(page, 'Homepage Tablet');
await browser.close();
})();
# التشغيل مع Percy
# تعيين متغير البيئة PERCY_TOKEN
export PERCY_TOKEN=your_token_here
npx percy exec -- node percy-test.js
# تقارن Percy لقطات الشاشة في السحابة الخاصة بهم
# عرض النتائج في: https://percy.io/your-org/your-project
أفضل ممارسة: قم بتشغيل الاختبارات البصرية في بيئة متسقة (نفس نظام التشغيل، إصدار المتصفح، الخطوط) لتجنب النتائج الإيجابية الكاذبة من اختلافات العرض. استخدم حاويات Docker أو خدمات CI/CD التي توفر بيئات متسقة.
Chromatic لـ Storybook
يتكامل Chromatic مع Storybook للاختبار البصري على مستوى المكونات:
# تثبيت Chromatic
npm install --save-dev chromatic
# تأكد من وجود قصص Storybook
// Button.stories.js
import Button from './Button';
export default {
title: 'Components/Button',
component: Button,
};
export const Primary = () => <Button variant="primary">انقر هنا</Button>;
export const Secondary = () => <Button variant="secondary">إلغاء</Button>;
export const Disabled = () => <Button disabled>معطل</Button>;
export const Loading = () => <Button loading>جاري التحميل...</Button>;
# تشغيل Chromatic
npx chromatic --project-token=your_token_here
# يلتقط Chromatic تلقائيًا لقطات شاشة لجميع القصص
# ويقارنها بخطوط الأساس
# تكامل CI/CD (.github/workflows/chromatic.yml)
name: Visual Tests
on: push
jobs:
chromatic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install dependencies
run: npm ci
- name: Run Chromatic
uses: chromaui/action@v1
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitZeroOnChanges: true
BackstopJS للاختبار البصري الشامل
BackstopJS هي أداة تراجع بصري قوية مفتوحة المصدر:
# تثبيت BackstopJS
npm install -g backstopjs
# تهيئة التكوين
backstop init
# backstop.json
{
"id": "my_app_visual_tests",
"viewports": [
{
"label": "phone",
"width": 375,
"height": 667
},
{
"label": "tablet",
"width": 768,
"height": 1024
},
{
"label": "desktop",
"width": 1920,
"height": 1080
}
],
"scenarios": [
{
"label": "Homepage",
"url": "http://localhost:3000/",
"delay": 1000,
"misMatchThreshold": 0.1
},
{
"label": "Products Page",
"url": "http://localhost:3000/products",
"delay": 1000,
"selectors": [".product-grid"],
"misMatchThreshold": 0.1
},
{
"label": "Login Form",
"url": "http://localhost:3000/login",
"delay": 500,
"selectors": ["form"],
"hideSelectors": ["#dynamic-ad"],
"misMatchThreshold": 0.1
},
{
"label": "Dashboard - Authenticated",
"url": "http://localhost:3000/dashboard",
"delay": 2000,
"cookiePath": "backstop_data/cookies.json",
"misMatchThreshold": 0.1
}
],
"paths": {
"bitmaps_reference": "backstop_data/bitmaps_reference",
"bitmaps_test": "backstop_data/bitmaps_test",
"html_report": "backstop_data/html_report"
},
"engine": "puppeteer",
"report": ["browser", "CI"],
"debug": false
}
# إنشاء خط الأساس (لقطات الشاشة المرجعية)
backstop reference
# تشغيل الاختبارات (مقارنة الحالية مع خط الأساس)
backstop test
# الموافقة على التغييرات (تحديث خط الأساس)
backstop approve
# إنشاء تقرير HTML تفاعلي
# يفتح تلقائيًا بعد تشغيل الاختبار
معالجة المحتوى الديناميكي
يمكن أن يتسبب المحتوى الديناميكي (الطوابع الزمنية، الرسوم المتحركة، الإعلانات) في نتائج إيجابية كاذبة. إليك كيفية التعامل معها:
// إخفاء العناصر الديناميكية قبل لقطة الشاشة
await page.evaluate(() => {
// إخفاء العناصر ذات المحتوى الديناميكي
document.querySelectorAll('.timestamp, .live-chat').forEach(el => {
el.style.visibility = 'hidden';
});
// تعطيل الرسوم المتحركة
document.querySelectorAll('*').forEach(el => {
el.style.animation = 'none';
el.style.transition = 'none';
});
});
// استبدال النص الديناميكي بعنصر نائب
await page.evaluate(() => {
document.querySelectorAll('.timestamp').forEach(el => {
el.textContent = '2024-01-01 00:00:00';
});
document.querySelectorAll('.random-id').forEach(el => {
el.textContent = 'PLACEHOLDER_ID';
});
});
// تجميد الوقت للطوابع الزمنية المتسقة
await page.evaluateOnNewDocument(() => {
const constantDate = new Date('2024-01-01T00:00:00Z');
Date = class extends Date {
constructor(...args) {
if (args.length === 0) {
super(constantDate);
} else {
super(...args);
}
}
};
Date.now = () => constantDate.getTime();
});
// انتظار اكتمال حالات التحميل
await page.waitForSelector('.skeleton-loader', { hidden: true });
await page.waitForFunction(() => {
return !document.querySelector('.loading');
});
أفضل ممارسات الاختبار البصري
اتبع هذه الممارسات للاختبار البصري الموثوق:
// 1. استخدام أحجام منفذ عرض متسقة
const viewports = {
mobile: { width: 375, height: 667 },
tablet: { width: 768, height: 1024 },
desktop: { width: 1920, height: 1080 },
};
// 2. انتظار تحميل الخطوط
await page.evaluateHandle('document.fonts.ready');
// 3. انتظار تحميل الصور
await page.evaluate(() => {
return Promise.all(
Array.from(document.images)
.filter(img => !img.complete)
.map(img => new Promise(resolve => {
img.onload = img.onerror = resolve;
}))
);
});
// 4. تعطيل رسوم CSS المتحركة
await page.addStyleTag({
content: `
*, *::before, *::after {
animation-duration: 0s !important;
transition-duration: 0s !important;
}
`
});
// 5. تعيين عتبة مقبولة
const threshold = 0.1; // يُسمح باختلاف بيكسل 0.1%
// 6. التقاط عناصر محددة، وليس الصفحة الكاملة
await page.screenshot({
path: 'screenshot.png',
clip: {
x: 0,
y: 0,
width: 1280,
height: 720,
}
});
// 7. استخدام سمات البيانات لتحديد المحتوى الديناميكي
// في HTML: <div data-visual-test-ignore>محتوى ديناميكي</div>
await page.evaluate(() => {
document.querySelectorAll('[data-visual-test-ignore]').forEach(el => {
el.style.visibility = 'hidden';
});
});
تحذير: الاختبارات البصرية أبطأ وأكثر هشاشة من اختبارات الوحدة. استخدمها بشكل استراتيجي لمكونات واجهة المستخدم الحرجة وتدفقات المستخدم، وليس كل صفحة واحدة. ادمج مع الاختبارات الوظيفية للتغطية الشاملة.
تكامل CI/CD
ادمج الاختبارات البصرية في خط أنابيب التكامل المستمر الخاص بك:
# مثال GitHub Actions
# .github/workflows/visual-tests.yml
name: Visual Regression Tests
on:
pull_request:
branches: [main]
jobs:
visual-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Start application server
run: npm start &
env:
NODE_ENV: test
- name: Wait for server
run: npx wait-on http://localhost:3000
- name: Run BackstopJS tests
run: |
npm install -g backstopjs
backstop test || backstop test --docker
- name: Upload diff images
if: failure()
uses: actions/upload-artifact@v3
with:
name: visual-test-diffs
path: backstop_data/bitmaps_test/**/
- name: Comment PR with results
if: failure()
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '⚠️ تم اكتشاف تراجع بصري! تحقق من القطع الأثرية لصور الفرق.'
})
# تكامل Percy CI
- name: Run Percy tests
run: npx percy exec -- node visual-tests.js
env:
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
الاختبار البصري على مستوى المكونات
اختبر المكونات الفردية بشكل منعزل:
// component-visual-test.js
const puppeteer = require('puppeteer');
const percySnapshot = require('@percy/puppeteer');
async function testComponent(componentName, states) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
for (const state of states) {
// الانتقال إلى صفحة عرض المكون
await page.goto(`http://localhost:6006/iframe.html?id=${componentName}--${state}`);
// انتظار عرض المكون
await page.waitForSelector('#root > *');
// التقاط لقطة الشاشة
await percySnapshot(page, `${componentName} - ${state}`);
}
await browser.close();
}
// اختبار مكون الزر في جميع الحالات
testComponent('button', [
'default',
'primary',
'secondary',
'disabled',
'loading',
'with-icon',
]);
// اختبار مكونات النموذج
testComponent('input-field', [
'empty',
'filled',
'error',
'disabled',
'focused',
]);
تمرين 1: قم بإعداد BackstopJS لمشروعك مع سيناريوهات تغطي: (1) الصفحة الرئيسية في 3 أحجام منفذ عرض، (2) حالات فتح/إغلاق قائمة التنقل، (3) حالات التحقق من النموذج (فارغ، ممتلئ، خطأ)، (4) مربعات الحوار المنبثقة، و(5) جداول البيانات مع الترقيم. قم بتكوين العتبات المناسبة وإخفاء العناصر الديناميكية.
تمرين 2: أنشئ إطار عمل اختبار تراجع بصري مخصص باستخدام Puppeteer وpixelmatch. نفذ: (1) إدارة خط الأساس، (2) توليد الفروقات التلقائي، (3) تقرير HTML مع مقارنات جنبًا إلى جنب، (4) عتبات قابلة للتكوين لكل اختبار، و(5) القدرة على الموافقة/رفض التغييرات.
تمرين 3: قم بدمج Percy أو Chromatic في خط أنابيب CI/CD الخاص بك. قم بتكوينه لـ: (1) التشغيل على جميع طلبات السحب، (2) حظر الدمج إذا تم اكتشاف تراجعات بصرية، (3) نشر صور الفرق في تعليقات PR، (4) الموافقة تلقائيًا على التغييرات في الفرع الرئيسي بعد المراجعة اليدوية.
الملخص
في هذا الدرس، غطينا استراتيجيات اختبار التراجع البصري الشاملة:
- فهم كيف يكمل اختبار التراجع البصري الاختبار الوظيفي
- تنفيذ مقارنة لقطات الشاشة الأساسية باستخدام Puppeteer وpixelmatch
- استخدام Laravel Dusk للاختبار البصري المستند إلى PHP
- الاستفادة من المنصات التجارية مثل Percy وChromatic
- إعداد BackstopJS للاختبار البصري مفتوح المصدر
- معالجة المحتوى الديناميكي والرسوم المتحركة في الاختبارات البصرية
- أفضل الممارسات للاختبارات البصرية الموثوقة والقابلة للصيانة
- دمج الاختبارات البصرية في خطوط أنابيب CI/CD
- استراتيجيات الاختبار البصري على مستوى المكونات
يكتشف اختبار التراجع البصري أخطاء واجهة المستخدم التي تفوتها الاختبارات الوظيفية الآلية. من خلال دمج الاختبار البصري في سير عمل التطوير الخاص بك، فإنك تضمن تجارب مستخدم متسقة ومصقولة عبر جميع الإصدارات.