بهبود سازگاری تست بصری سیستم طراحی با Docker
تست بصری یک راه عالی برای آزمایش سازگاری مؤلفه UI شما در یک سیستم طراحی است. با این حال، هنگام کار بر روی یک پروژه با چندین توسعه دهنده و تلاش برای اجرای آزمایش به صورت محلی یا CI، ممکن است مشکل ساز شود، زیرا ممکن است رابط کاربری برای هر توسعه دهنده متفاوت به نظر برسد. بنابراین لازم است که تست ها را روی همان مرورگر و محیط اجرا کنید تا از سازگاری اطمینان حاصل شود.
Docker یک پلتفرم منبع باز است که به توسعه دهندگان این امکان را می دهد تا برنامه ها را در داخل کانتینرهای ایزوله ایجاد، استقرار و اجرا کنند. با استفاده از Docker، میتوانیم از نتایج ثابت در محیطهای مختلف و نسخه مرورگر اطمینان حاصل کنیم.
این پروژه از یک سیستم طراحی عالی الهام گرفته شده است که آزمایش بصری خود را پیاده سازی کرده است، که الهام بخش من برای کشف نحوه عملکرد آن بود. می توانید اجرای کامل این پروژه را در این مخزن GitHub مشاهده کنید. حالا بیایید در مورد نحوه اجرای آن بحث کنیم.
ایجاد مؤلفه رابط کاربری
این پروژه از لرنا برای مدیریت بسته های خود استفاده می کند
- packages
- core // Core component
- design-tokens // Tokens
- visual-test // UI Testing
-
core
بسته: این جزء اصلی سیستم طراحی است. ما از Vue 3، Vite و TypeScript استفاده می کنیم. ما همچنین از Storybook برای نمایش موارد استفاده کامپوننت و Jest برای تست واحد استفاده می کنیم. -
design-tokenks
بسته: ما توکن ها را از این بسته تولید می کنیم. -
visual-testing
بسته: ما اجزای UI را از این بسته آزمایش می کنیم.
تست بصری ایجاد کنید
این بسته از چندین کتابخانه استفاده می کند
-
jest
: ما از آن برای ایجاد موارد آزمایشی خود استفاده می کنیم. -
jest-html-reporter
: ما یک گزارشگر سفارشی ایجاد می کنیم تا نشان دهیم کدام تست های رابط کاربری ناموفق هستند. -
jest-image-snapshot
: ما از این کتابخانه برای گرفتن صفحه رابط کاربری و بررسی تغییرات بین تست ها استفاده می کنیم. -
jest-puppeteer-docker
: ما از این کتابخانه برای اجرای Puppeteer در Docker استفاده می کنیم. -
puppeteer
: ما از این کتابخانه برای تعامل با مرورگر برای باز کردن صفحه برای بررسی تغییرات بصری استفاده می کنیم.
پیش نیازها
قبل از اجرای تست، باید Docker را نصب کنیم. ساده ترین راه برای انجام آن نصب Docker Desktop است. ما همچنین باید کتاب داستان را از روی بسازیم core
بسته تا بتوانیم از آن برای تست بصری استفاده کنیم.
راه اندازی بله
ما پیکربندی Jest را برای پشتیبانی از تست ویژوال تنظیم کردیم.
module.exports = {
preset: 'jest-puppeteer-docker',
// specify a list of setup files to be executed after the test framework has been set up but before any test suites are run.
setupFilesAfterEnv: ['./jest-setup/test-environment-setup.js'],
// executed once before any test suites are run
globalSetup: './jest-setup/setup.js',
// The function will be triggered once after all test suites
globalTeardown: './jest-setup/teardown.js',
testMatch: ['**/?(*.)+(visual.spec).[tj]s?(x)'],
modulePathIgnorePatterns: ['<rootDir>/dist/'],
// add jest-html-reporter to be our costume reporters
reporters: [
'default',
[
'jest-html-reporter',
{
outputPath: './visual-test-result/index.html',
pageTitle: 'Test Result',
includeFailureMsg: true,
// Path to a javascript file that should be injected into the test report,
customScriptPath: './inject-fail-images.js',
},
],
],
};
سپس، فایل راه اندازی را از فایل ساخت Storybook به مقداردهی اولیه سرور HTTP اضافه می کنیم.
const { setup: setupPuppeteer } = require('jest-puppeteer-docker');
const path = require('path');
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
// parse URL
const url = new URL(req.url, `http://${req.headers.host}`);
// serve static files from "static" directory
const filePath = path.join(__dirname, '../storybook-static', url.pathname);
fs.readFile(filePath, (err, data) => {
if (err) {
// if file not found, return 404 error
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.write('404 Not Found');
res.end();
} else {
// if file found, return file contents
res.writeHead(200, { 'Content-Type': getContentType(filePath) });
res.write(data);
res.end();
}
});
});
// helper function to get content type based on file extension
function getContentType(filePath) {
const extname = path.extname(filePath);
switch (extname) {
case '.html':
return 'text/html';
case '.css':
return 'text/css';
case '.js':
return 'text/javascript';
case '.json':
return 'application/json';
case '.png':
return 'image/png';
case '.jpg':
case '.jpeg':
return 'image/jpeg';
default:
return 'application/octet-stream';
}
}
module.exports = async (jestConfig) => {
// start server on port 3000
global.__SERVER__ = server.listen(3000, () => {
console.log('Server started on port 3000');
});
await setupPuppeteer(jestConfig);
};
سپس، یک پیکربندی پارگی اضافه می کنیم. در این پیکربندی، سرور HTTP را می بندیم و نتایج تست را در فایل دایرکتوری گزارشگر کپی می کنیم. برای اطلاعات بیشتر در مورد تنظیمات و تنظیمات حذف، میتوانید مستندات را مطالعه کنید jest-puppeteer-docker
.
const fs = require('fs');
const fse = require('fs-extra');
const path = require('path');
const { teardown: teardownPuppeteer } = require('jest-puppeteer-docker');
module.exports = async function globalTeardown(jestConfig) {
global.__SERVER__.close();
await teardownPuppeteer(jestConfig);
const dirname = path.join(__dirname, '..');
fs.copyFileSync(
`${dirname}/jest-reporters/inject-fail-images.js`,
`${dirname}/visual-test-result/inject-fail-images.js`
);
try {
fse.copySync(
`${dirname}/src/__image_snapshots__/__diff_output__`,
`${dirname}/visual-test-result/__diff_output__`,
{
overwrite: true,
}
);
} catch {}
};
بعد اضافه می کنیم jest-puppeteer-docker
پیکربندی تنظیمات دیگری را اینجا ببینید.
const getConfig = require('jest-puppeteer-docker/lib/config');
const baseConfig = getConfig();
const customConfig = Object.assign(
{
connect: {
defaultViewport: {
width: 1040,
height: 768,
},
},
browserContext: 'incognito',
chromiumFlags: '–ignore-certificate-errors',
},
baseConfig
);
module.exports = customConfig;
در نهایت، یک تابع سراسری برای پیمایش به صفحه کتاب داستان و یک تابع سراسری دیگر برای انجام تست بصری اضافه می کنیم.
const { toMatchImageSnapshot } = require('jest-image-snapshot');
jest.setTimeout(10000);
expect.extend({ toMatchImageSnapshot });
global.goto = async (id) => {
await global.page.goto(
`http://host.docker.internal:3000/iframe.html?id=${id}&viewMode=story`
);
await page.waitForNavigation({
waitUntil: 'networkidle0',
});
};
global.testUI = async () => {
await global.page.waitForSelector('#root');
const previewHtml = await global.page.$('body');
expect(await previewHtml.screenshot()).toMatchImageSnapshot();
};
راه اندازی گزارشگر است
پس از اتمام تست، معمولاً می خواهیم نتایج آزمایش را بررسی کنیم. اگر تست های ناموفق وجود داشته باشد، می توانیم نتیجه مقایسه را در اینجا نمایش دهیم packages/visual-test/visual-test-result/
.
document.addEventListener('DOMContentLoaded', () => {
[...document.querySelectorAll('.failureMsg')].forEach((fail, i) => {
const imagePath = `__diff_output__/${
(fail.textContent.split('__diff_output__/')[1] || '').split('png')[0]
}png`;
if (imagePath) {
const div = document.createElement('div');
div.style = 'margin-top: 16px';
const a = document.createElement('a');
a.href = `${imagePath}`;
a.target = '_blank';
const img = document.createElement('img');
img.src = `${imagePath}`;
img.style = 'width: 100%';
a.appendChild(img);
div.appendChild(a);
fail.appendChild(div);
}
});
});
اضافه کردن تست ها
برای افزودن تستهای جدید، معمولاً گرفتن اسکرین شات از هر صفحه کتاب داستان آسانتر است. به مثال زیر نگاه کنید، جایی که ما یک جزء دکمه با 5 صفحه مختلف داریم.
سپس، ما تست موردی را برای هر صفحه اضافه می کنیم.
describe('Button', () => {
test.each([['variants'], ['size'], ['disabled'], ['full-width']])(
'%p',
async (variant) => {
await global.goto(`buttons-button--${variant}`);
await global.page.evaluateHandle(`document.querySelector(".c-button")`);
await global.testUI();
}
);
});
همچنین اگر میخواهید بعد از عمل عنصر خاصی از صفحه نمایش بگیرید، میتوانید از Puppeteer API استفاده کنید.
در حال اجرا تست
برای اجرای تست ها می توانید مراحل زیر را دنبال کنید:
- با استفاده از کتاب داستان بسازید
yarn workspace @contra-ui/vue build-storybook --quiet
- فایل ساخت Storybook را از کپی کنید
core
بسته بهvisual-test
با استفاده ازyarn workspace @contra-ui/visual-test copy
- دسکتاپ Docker خود را باز کنید و با استفاده از آن تست را اجرا کنید
yarn workspace @contra-ui/visual-test test
. اگر برای اولین بار تست را اجرا کنید، مدتی طول می کشد تا داکر تصویر را دانلود کند و ظرف را بسازد. - نتیجه آزمایش را در
packages/visual-test/src/image_snapshots
برای دیدن اسکرین شات اگر مورد انتظار است
بررسی تست های شکست خورده
برای بررسی تست ناموفق می توانید فایل گزارش HTML را در آن باز کنید packages/visual-test/visual-test-result/
.
Jest تفاوت ها را در یک پوشه قرار می دهد که می توانید در آن بررسی کنید packages/visual-test/src/__image_snapshots__/__diff_output__/
. برای به روز رسانی آزمون، می توانید آن را اضافه کنید -u
پرچم: yarn workspace @contra-ui/visual-test test
راه اندازی CI
سپس مرحله ای را که به صورت محلی انجام داده ایم به CI منتقل می کنیم. سه مرحله اضافی وجود دارد که برای ایجاد خودکار روابط عمومی برای به روز رسانی تست های ناموفق مورد نیاز است.
name: build-pr
on:
pull_request:
branches:
- main
jobs:
install-dependency:
name: Install depedency
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16
cache: 'yarn'
- name: Install dependencies
run: yarn --immutable
visual-tests:
name: Visual tests
needs: [install-dependency]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16
cache: 'yarn'
- name: Install dependencies
run: yarn --immutable
- name: Run Lerna bootstrap
run: yarn lerna:bootstrap
- name: Build Storybook
run: yarn workspace @contra-ui/vue build-storybook --quiet
- name: Copy Storybook for visual tests
run: yarn workspace @contra-ui/visual-test copy
- name: Run visual tests
run: yarn workspace @contra-ui/visual-test test -u
- name: Check for any snapshots changes
run: sh scripts/porcelain.sh
- name: Set patch branch name
if: failure()
id: vars
run: echo ::set-output name=branch-name::"visual-snapshots/${{ github.head_ref }}"
- name: Create pull request with new snapshots
if: failure()
uses: peter-evans/create-pull-request@v4
with:
commit-message: 'test(visual): update snapshots'
title: 'update visual snapshots: ${{ github.event.pull_request.title }}'
body: This is an auto-generated PR with visual snapshot updates for \#${{ github.event.number }}.
labels: automated pr
branch: ${{ steps.vars.outputs.branch-name }}
base: ${{ github.head_ref }}
-
گام
name: Check for any snapshots changes
در این مرحله بررسی می کنیم که آیا commit جدیدی وجود دارد یا خیر. چون ما تنظیم کردیم-u
پرچم، به طور خودکار فایل موجود را تغییر می دهد. وقتی هر فایل اصلاح شده ای را پیدا می کنیم، اسکریپت با یک کد خروج غیر صفر خارج می شود.echo "--------" echo "Checking for uncommitted build outputs..." if [ -z "$(git status --porcelain)" ]; then echo "Working copy is clean" else echo "Another Pull Request has been created. Please check it to accept or reject the visual changes." git status exit 1 fi
-
گام
name: Set patch branch name
در این مرحله اگر مراحل قبلی شکست خورده باشد، نام شاخه را به عنوان متغیر خروجی ذخیره می کنیم. -
گام
name: Create pull request with new snapshots
در این مرحله، ما یک درخواست کشش برای شاخه شکست خورده ایجاد می کنیم، این مرحله برای ما آسان تر می کند که ببینیم چه رابط کاربری تغییر کرده است و در صورت انتظار می توانیم تغییرات را بپذیریم، در اینجا مثال درخواست pull است:
همچنین باید مجوزهای گردش کار خود را در صفحه تنظیمات به روز کنید تا اکشن GitHub برای ایجاد درخواست کشش فعال شود.
استفاده از داکر می تواند واقعاً به طور کامل مورد استفاده قرار گیرد تا آزمایشات بصری ما را هم به صورت محلی و هم در CI سازگارتر کند. امیدوارم این پست به شما ایده هایی داده باشد. شما می توانید کد کامل را در این مخزن GitHub مشاهده کنید.