برنامه نویسی

بهبود سازگاری تست بصری سیستم طراحی با 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 مشاهده کنید.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا