IlmHamroh
JavaScript Full-stack/22-qism. Frontend testlash12/16-dars22 daqiqa
Mundarija (36)

Storybook II: play funksiyasi, a11y addon va Vitest bilan story testlari

Qisqacha: Story'ning play funksiyasi u chizilgandan keyin ishlaydi. Unda element topiladi (canvas), bosiladi (userEvent) va tekshiriladi (expect) — RTL testining o'zi, faqat story ichida. Vitest addon'i (@storybook/addon-vitest) har story'ni Browser Mode testiga aylantiradi: story xatosiz chizildimi, play o'tdimi. A11y addon'i (@storybook/addon-a11y) har story'ni axe bilan tekshiradi va a11y.test: "error" bo'lsa buzilishda testni yiqitadi. Kanon vazifalar da 12 story = 12 test, ~15 s; a11y birinchi o'tishda haqiqiy xato topdi.

Bu darsda

  • play funksiyasini yozasiz: canvas, userEvent, args, step, waitFor.
  • storybook/test dagi fn, expect, waitFor qayerdan kelishini tushuntirasiz.
  • @storybook/addon-vitest bilan story'larni npm run test:storybook orqali yurgizasiz.
  • @storybook/addon-a11y va a11y.test ning uch qiymatini qo'llaysiz.
  • Chromatic va "jonli hujjat" g'oyasini bilasiz — qachon kerak va narxi nima.

Oldin bilishingiz kerak: Storybook I, Vitest Browser Mode, RTL II: foydalanuvchi harakatlari, React'da accessibility.

1. Nega bu kerak?

Oldingi darsda vazifalar ga 9 ta story qo'shdik. Ular chiroyli ko'rinadi — lekin faqat kimdir Storybook'ni ochib qaraganda. Sardor Tugma ga yangi prop qo'shib, OddiyQator ni sindirib qo'ysa, buni kim ko'radi? Hech kim, toki kimdir o'sha story'ni ochmaguncha.

E'tibor bering: har story — tayyor test sharti. OddiyQator/Faol — "faol vazifa qatori xatosiz chiziladi". Tugma/Xavfli — "xavfli tugma o'chirish matni bilan chiqadi". Biz ularni allaqachon yozdik. Qolgani — ikki narsa: (1) story'larni avtomatik yurgizish, (2) "bossam nima bo'ladi?" degan savolni story ichida yozish.

Hayotiy o'xshatish: oldingi darsdagi tatib ko'rish stolida oshpaz har likopchaga qaraydi. Endi u har likopchaga kartochka qo'yadi: "tuzi — 1 choy qoshiq, 70 °C dan issiq". Har ertalab yordamchi kartochkalar bo'yicha tekshirib chiqadi. Story — likopcha, play — kartochka, Vitest — har ertalabki yordamchi.

flowchart LR
  S["story fayli"] --> V["storybookTest (Vitest plagini)"]
  V --> B["Browser Mode: Chrome"]
  B --> R["1. chizish"]
  R --> P["2. play: bosish + expect"]
  P --> A["3. axe: a11y"]
  A --> T["✓ yoki ×"]

Rasmda: story fayli Vitest plagini orqali testga aylanadi va haqiqiy brauzerda uch bosqichdan o'tadi. Uchalasidan biri yiqilsa — test qizil.

2. play funksiyasi

2.1 Kanondagi birinchi play

vazifalar dagi Tugma story fayliga bitta story qo'shildi:

tsx
export const Bosiladi: Story = {
  play: async ({ args, canvas, userEvent }) => {
    await userEvent.click(
      canvas.getByRole("button", { name: "Saqlash" }),
    );
    await expect(args.onClick).toHaveBeenCalledOnce();
  },
};

play — story chizilgandan keyin Storybook chaqiradigan async funksiya. U bitta obyekt oladi (story konteksti):

Maydon Nima
canvas story chizilgan joy uchun so'rovlar — within(…) ning o'zi: getByRole, findByText …
userEvent RTL II dagi user-event, Storybook'ga ulangan
args story'ning birlashgan args'i — fn() josuslari ham shu yerda
step harakatlarni nomli guruhlarga bo'lish (Interactions panelida ko'rinadi)
mount chizishdan oldin biror ish qilish kerak bo'lsa (sanani soxtalash)

Tanish narsalar: getByRole — RTL I, toHaveBeenCalledOnce — Vitest II. Yangi narsa faqat joy: test alohida *.test.tsx da emas, story ichida.

expect, fn, waitFor, userEvent — storybook/test paketidan (Storybook 9 dan beri yadroning ichida). Ichida Vitest'ning expect va josuslari, Testing Library'ning user-event va waitFor — Storybook ularni o'rab, har qadamni Interactions paneliga yozadi. Shuning uchun expect oldida await: panel har tekshiruvni qadam sifatida ko'rsatadi, xatoda qaysi qadamda to'xtaganini aytadi.

2.2 Natija oynasida: story + play, o'zimiz yurgizamiz

Storybook natija oynasida yo'q. Lekin oldingi darsdagi kabi, CSF oddiy obyekt — play esa oddiy async funksiya. Storybook nima qilishini kichik o'quv modelida takrorlaymiz: chizamiz, canvas ni within(container) qilib beramiz, play ni chaqiramiz.

jsx
import { render, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { expect, test, vi } from "vitest";

function PortionPicker({ start = 1, onChange }) {
  const [count, setCount] = useState(start);
  const change = (next) => {
    setCount(next);
    onChange(next);
  };
  return (
    <div role="group" aria-label="Porsiya">
      <button onClick={() => change(count - 1)} disabled={count <= 1}>
        Kamaytirish
      </button>
      <output aria-label="Soni">{count}</output>
      <button onClick={() => change(count + 1)}>Ko'paytirish</button>
    </div>
  );
}

// PortionPicker.stories.jsx
const meta = {
  component: PortionPicker,
  args: { onChange: vi.fn() },
};

export const Increments = {
  play: async ({ args, canvas, userEvent }) => {
    const plus = canvas.getByRole("button", { name: "Ko'paytirish" });
    await userEvent.click(plus);
    await userEvent.click(plus);
    const output = canvas.getByLabelText("Soni");
    await expect(output).toHaveTextContent("3");
    await expect(args.onChange).toHaveBeenLastCalledWith(3);
  },
};

export const MinimumIsOne = {
  args: { start: 1 },
  play: async ({ canvas }) => {
    const minus = canvas.getByRole("button", { name: "Kamaytirish" });
    await expect(minus).toBeDisabled();
  },
};

// Storybook'ning ishi — o'quv modeli: chiz, keyin play
async function runStory(meta, story) {
  const args = { ...meta.args, ...story.args };
  const Component = meta.component;
  const { container } = render(<Component {...args} />);
  const canvas = within(container);
  await story.play?.({ args, canvas, userEvent: userEvent.setup() });
}

test("Increments", () => runStory(meta, Increments));
test("MinimumIsOne", () => runStory(meta, MinimumIsOne));

Konsolda:

text
  ✓ Increments
  ✓ MinimumIsOne
Testlar: 2 o'tdi (jami 2)

Uch narsaga qarang:

  • play ichida canvas.getByRole — screen emas. Storybook bir sahifada bir nechta story'ni (Docs sahifasida) chizadi; canvas qidiruvni faqat shu story bilan cheklaydi.
  • story.play?.(…) — play siz story ham story: u faqat "xatosiz chizildi" deb tekshiriladi. Bunday testni tutun testi (smoke test) deyishadi — "yoqib ko'rdik, tutun chiqmadi".
  • MinimumIsOne da harakat yo'q, faqat expect — play holatni tekshirish uchun ham yoziladi.

runStory — modelning yuragi, haqiqiy Storybook'da undan ancha ko'p ish bor: dekoratorlar, loaders, parameters, har qadamni panelga yozish, josuslarni tozalash. Lekin tartib shu: args → chizish → play.

2.3 waitFor: hodisa keyingi task'da

Kanondagi uchinchi play — OchirishDialog/Tasdiq:

tsx
export const Tasdiq: Story = {
  play: async ({ args, canvas, userEvent }) => {
    await userEvent.click(
      canvas.getByRole("button", { name: "O'chirish" }),
    );
    // close hodisasi — keyingi task'da (form method="dialog")
    await waitFor(() =>
      expect(args.onJavob).toHaveBeenCalledWith(true),
    );
  },
};

Birinchi variantda waitFor yo'q edi — test yiqildi. Sabab: <form method="dialog"> dagi tugma bosilganda brauzer dialogni yopadi, close hodisasi esa keyingi taskda keladi (event loop). click tugadi, onJavob hali chaqirilmagan. Xuddi shu holatni natija oynasida kechikkan javob bilan ko'ramiz:

jsx
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, test, vi } from "vitest";

// Javob keyinroq keladi — dialog close hodisasidagi kabi
function ConfirmDelete({ onAnswer }) {
  const answer = (value) => setTimeout(() => onAnswer(value), 50);
  return (
    <div role="dialog" aria-label="Osh o'chirilsinmi?">
      <button onClick={() => answer(false)}>Bekor qilish</button>
      <button onClick={() => answer(true)}>O'chirish</button>
    </div>
  );
}

test("waitFor'siz", async () => {
  const onAnswer = vi.fn();
  const user = userEvent.setup();
  render(<ConfirmDelete onAnswer={onAnswer} />);
  await user.click(screen.getByRole("button", { name: "O'chirish" }));
  expect(onAnswer).toHaveBeenCalledWith(true);
});

test("waitFor bilan", async () => {
  const onAnswer = vi.fn();
  const user = userEvent.setup();
  render(<ConfirmDelete onAnswer={onAnswer} />);
  await user.click(screen.getByRole("button", { name: "O'chirish" }));
  await waitFor(() => expect(onAnswer).toHaveBeenCalledWith(true));
});

Konsolda:

text
  × waitFor'siz
    → expected "vi.fn()" to be called with arguments: [ true ]
Number of calls: 0
  ✓ waitFor bilan
Testlar: 1 yiqildi, 1 o'tdi (jami 2)

Nega 50 ms, 0 emas? Biz avval setTimeout(…, 0) ni sinadik — ikkala test ham o'tdi. Sabab: user-event har harakatdan keyin delay qadar kutadi (sukut 0 — bitta setTimeout(0)), va shu kutish ichida 0 ms li javob ham kelib ulguradi. Kanondagi dialogda bu yetmadi. Qoida o'zgarmaydi: "keyinroq" keladigan narsaga — waitFor, vaqtni taxmin qilishga emas.

waitFor ichidagi funksiyani u o'tguncha (sukut bo'yicha 1 s gacha, har 50 ms) qayta chaqiradi (RTL II). Number of calls: 0 — josus umuman chaqirilmagan: "argument boshqa" emas, "chaqiruv yo'q". Haqiqiy Vitest 5.0.4 da ham (biz alohida ishga tushirdik) xabar shunday, faqat oldida bo'sh qator va kod bo'lagi bor — natija oynasi konsoli bo'sh qatorlarni tashlab yuboradi.

2.4 step: uzun play ni bo'lish

Ko'p qadamli story'da (forma: ism, telefon, yuborish) step harakatlarni nomlaydi:

tsx
export const Yuborildi: Story = {
  play: async ({ canvas, userEvent, step }) => {
    await step("Formani to'ldirish", async () => {
      await userEvent.type(canvas.getByLabelText("Ism"), "Dilshod");
      await userEvent.type(
        canvas.getByLabelText("Telefon"),
        "+998 90 000 00 00",
      );
    });
    await step("Yuborish", async () => {
      const send = canvas.getByRole("button", { name: "Bron" });
      await userEvent.click(send);
    });
  },
};

Interactions panelida ikkita yig'iladigan guruh chiqadi. Testga ta'siri yo'q — faqat o'qish va xatoni topish oson. vazifalar da play lar qisqa, step ishlatilmagan.

Tekshirib ko'ring: play ichida screen.getByRole("button") yozilsa, Storybook'ning Docs sahifasida nima bo'lishi mumkin?

Javob

Docs sahifasida bir faylning hamma story'lari bitta sahifada chiziladi — screen hammasini ko'radi va Found multiple elements… xatosi chiqadi (yoki boshqa story'ning tugmasi bosiladi). canvas faqat shu story ichidan qidiradi.

3. @storybook/addon-vitest: story = test

3.1 O'rnatish

bash
git switch -c test/storybook-vitest
npm i -D -E @storybook/addon-vitest@10.6.1 \
  @storybook/addon-a11y@10.6.1

+3 paket (ikki addon va axe-core), 13 s. main.ts da:

ts
  // 22/#12: a11y paneli (axe) va story'larni
  // Vitest testiga aylantirish
  addons: ["@storybook/addon-a11y", "@storybook/addon-vitest"],

3.2 vitest.config.ts — beshinchi loyiha

Browser Mode darsida vitest.config.ts da loyihalar (mantiq, komponent, brauzer, tarmoq) bor edi. Yana bittasi qo'shildi:

ts
import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
ts
      {
        // 22/#12: har story — test: render (xatosiz
        // chiziladi), play funksiyasi (harakat + expect)
        // va a11y (axe, preview.tsx). Story fayllarining
        // o'zi — testlar; alohida *.test.tsx yozilmaydi
        extends: true,
        plugins: [storybookTest({ configDir: ".storybook" })],
        test: {
          name: "storybook",
          browser: {
            enabled: true,
            headless: true,
            provider: playwright({
              launchOptions: { channel: "chrome" },
            }),
            instances: [{ browser: "chromium" }],
          },
        },
      },
  • storybookTest({ configDir }) — Vitest plagini: .storybook/main.ts dagi stories globini o'qiydi va har story'ni test qiladi. include yozilmaydi — fayllar ro'yxati Storybook'dan.
  • browser — #08 dagi o'sha sozlama: tizimdagi Chrome, headless, brauzer yuklanmaydi.
  • Skript: "test:storybook": "vitest run --project storybook". npm test ga kirmadi — Chrome talab qiladi va sekinroq.

Bu darsdagi kanon izohlari telefonga sig'ishi uchun qatorlarga bo'lingan — kod va ma'no o'zgarmagan.

3.3 Qo'lda yozilmagan fayl

Eski qo'llanmalarda .storybook/vitest.setup.ts bor: unda setProjectAnnotations([...]) chaqiriladi. vazifalar da bu fayl yo'q. Storybook 10 da addon annotatsiyalarni (preview'dagi dekoratorlar, a11y sozlamasi) o'zi qo'llaydi, MIGRATION.md esa ogohlantiradi: qo'lda chaqirish addon'ning o'z annotatsiyalarini almashtirib yuborishi mumkin. Eski maqolada ko'rsangiz — Storybook versiyasini tekshiring.

3.4 Natija

bash
npm run test:storybook
text
 Test Files  4 passed (4)
      Tests  12 passed (12)
   Duration  9.99s (setup 54%, tests 33%, worker 8%, import 4%)

9 ta eski story + 3 ta play li = 12 test. Devor vaqti 14.8–15.1 s (uch o'lchov), Duration 10.0–10.9 s. Vaqtning yarmidan ko'pi setup — brauzer va Storybook preview'ni tayyorlash. Taqqoslash uchun — bitta test o'rtacha:

Bitta test o'rtacha (Duration / testlar soni)
  • Vitest jsdom (hamma test)348 test, 12.68 s36 ms
  • Browser Mode6 test, 8.33 s1 390 ms
  • Storybook (Browser Mode)12 test, 9.99 s830 ms

Manba: O'lchandi: vazifalar kanoni (holat 12, tekshiruv-12), i5-12500H, Windows 11, Node 24.21.0, Vitest 5.0.4, Chrome 155, 2026-10-20

Grafik Duration ni testlar soniga bo'ladi — birinchi darsdagi grafik bilan bir xil usul. Ishga tushish narxi ham ichida, shuning uchun test qancha ko'p bo'lsa, bittasiga shuncha kam tushadi. O'sha 6 ta brauzer testi holat 08 da 3.84 s (≈0.64 s/test, birinchi darsdagi ≈0.7 s) edi, holat 12 da 8.33 s — bir xil test, kompyuter yuki boshqa. Shuning uchun aniq soniyaga emas, tartibga qarang. Xulosa o'zgarmaydi: brauzerdagi test jsdom'dagidan o'nlab baravar qimmat. Story testlari jsdom testlarini almashtirmaydi — ular brauzerga muhtoj komponentlar va "har holat xatosiz chiziladi" kafolati uchun.

3.5 Birinchi o'tish: 12 dan 5–6 tasi qizil

Birinchi ishga tushirishda testlarning deyarli yarmi yiqildi (alohida nusxada o'lchandi):

Nechta Nima Tuzatish
4 router xatosi (basename) preview.tsx dekoratori (o'shanda hali yo'q edi)
1 Tasdiq — onJavob chaqirilmagan waitFor («waitFor: hodisa keyingi task'da» bo'limi)
1 UchVazifa — a11y keyingi bo'lim

Diqqat: oldingi darsda story'lar Storybook'da ko'rinib turgandek edi — router xatosi faqat ochilgan story'da qizil qutida chiqadi. Kimdir hamma story'ni ochib chiqmaguncha, xato yashiringan. Test hammasini har safar ochadi.

4. @storybook/addon-a11y: axe har story'da

4.1 Sozlash

preview.tsx ga:

tsx
  parameters: {
    // 22/#12: addon-a11y — har story axe bilan
    // tekshiriladi; "error" — buzilish bo'lsa Vitest'da
    // test YIQILADI ("todo" — faqat ogohlantirish)
    a11y: { test: "error" },
  },

axe (axe-core) — Deque kompaniyasining ochiq kodli a11y tekshiruvchisi (HTML a11y va 18-qism da ko'rgan edik). Addon har story chizilgach (va play dan keyin) axe'ni o'sha story ustida ishga tushiradi. Storybook'dagi Accessibility panelida natija: buzilishlar, o'tganlar, tekshirib bo'lmaganlar.

a11y.test uch qiymat oladi:

Qiymat Vitest'da
"error" buzilish — test yiqiladi
"todo" buzilish — ogohlantirish, test o'tadi
"off" axe testda ishlamaydi

Butun loyiha uchun "error", hali tuzatilmagan bitta story uchun parameters: { a11y: { test: "todo" } } — eski loyihaga a11y'ni bosqichma-bosqich kiritishning yo'li.

4.2 Biz topdik: <li> <ul> siz

Birinchi o'tishda VazifaRoyxati/UchVazifa yiqildi:

text
<li> elements must be contained in a <ul> or <ol> (listitem)

Tarjima: "<li> elementlari <ul> yoki <ol> ichida bo'lishi kerak (listitem qoidasi)". VazifaRoyxati faqat <li> larni qaytaradi, <ul> esa App da turadi — ilovada hammasi to'g'ri. Story komponentni yolg'iz chizgani uchun <li> lar ota-onasiz qoldi. Ekran o'quvchi bunday ro'yxatni "ro'yxat, 3 band" deb e'lon qila olmaydi.

Tuzatish — story o'ramida, ilovada emas:

tsx
        {/* Komponent faqat <li> lar beradi — <ul> App da
            (a11y: listitem) */}
        <ul>
          <Story />
        </ul>

Xato ilovada emas, story'da edi — lekin baribir foydali topilma: u komponentning shartnomasini ko'rsatdi ("meni <ul> ichiga qo'ying"). Izoh shu shartnomani yozib qo'ydi.

4.3 Tajriba: nomsiz tugma

Addon haqiqatan yiqitadimi — ataylab tekshirdik: vaqtinchalik ZzSinov.stories.tsx da matnsiz <button>. Haqiqiy Vitest chiqishi (qisqartirilmagan):

text
 RUN  v5.0.4 C:/Users/…/vazifalar
   × Nomsiz Tugma 1471ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
 FAIL  |storybook (chromium)| stories/ZzSinov.stories.tsx > Nomsiz Tugma
Error: 
Click to debug the error directly in Storybook: http://localhost:6006/?path=/story/zzsinov--nomsiz-tugma&addonPanel=storybook/interactions/panel
expect(received).toHaveNoViolations(expected)
Expected the HTML found at $('.tugma') to have no violations:
<button type="button" class="tugma"></button>
Received:
"Buttons must have discernible text (button-name)"
Fix any of the following:
  Element does not have inner text that is visible to screen readers
  aria-label attribute does not exist or is empty
  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty
  Element has no title attribute
  Element does not have an implicit (wrapped) <label>
  Element does not have an explicit <label>
  Element's default semantics were not overridden with role="none" or role="presentation"
You can find more information on this issue here: 
https://dequeuniversity.com/rules/axe/4.14/button-name?application=axeAPI
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
 Test Files  1 failed (1)
      Tests  1 failed (1)
   Start at  11:12:41

O'qiymiz:

  • |storybook (chromium)| — loyiha nomi va brauzer; keyin fayl va story nomi (Nomsiz Tugma — eksport nomi NomsizTugma dan).
  • Buttons must have discernible text (button-name) — "tugmaning ajratib bo'ladigan matni bo'lishi kerak". Qavsda qoida nomi.
  • Fix any of the following — "quyidagilardan birini tuzating": ichki matn, aria-label, aria-labelledby, title… Bittasi yetarli.
  • Oxiridagi havola — qoidaning to'liq tavsifi (axe 4.14 — addon o'zi olib kelgan versiya).

vazifalar ning o'z Tugma sida bu xato bo'lishi mumkin emas: 18-qismda turlar matnsiz va aria-label siz tugmani tsc darajasida taqiqlagan. Tajriba fayli o'chirildi.

axe'ning natija oynasida ishlaydigan nusxasi yo'q. Uning bitta qoidasini — "har tugmaning nomi bor" — RTL bilan o'zimiz yozsak bo'ladi:

jsx
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";

function Toolbar() {
  return (
    <div role="toolbar" aria-label="Buyurtma">
      <button>Saqlash</button>
      <button>
        <svg aria-hidden="true" width="16" height="16" />
      </button>
    </div>
  );
}

test("har tugmaning nomi bor", () => {
  render(<Toolbar />);
  for (const btn of screen.getAllByRole("button")) {
    expect(btn).toHaveAccessibleName(/.+/);
  }
});

Konsolda:

text
  × har tugmaning nomi bor
    → expected <button> to have accessible name /.+/, received ''
Testlar: 1 yiqildi, 0 o'tdi (jami 1)

/.+/ — "kamida bitta belgi" (RegExp). Birinchi tugma o'tdi, ikkinchisining nomi bo'sh. Bu — axe'ning yuzlab qoidasidan bittasi; to'liq a11y testlari Accessibility testlari da.

Tekshirib ko'ring: Jamoangiz 200 story'li eski loyihaga a11y addon qo'shdi, 40 story qizil. Ertaga reliz. Nima qilasiz?

Javob

preview da a11y: { test: "todo" } — hamma buzilish ogohlantirish bo'lib ko'rinadi, reliz to'xtamaydi. Keyin story'larni bittalab tuzatib, har tuzatilgan faylda "error" ga o'tkazasiz (yoki aksincha: umumiy "error", 40 story'da vaqtincha "todo"). Muhimi — ro'yxat ko'rinib tursin; "off" bilan muammo yo'qolmaydi, faqat ko'rinmay qoladi.

5. Qamrov, CI va narx

5.1 Story testlari qamrovga kirmadi

Tugma va OddiyQator Vitest II dagi qamrov hisobotida 0 % edi — endi story'lar ularni chizadi. Lekin npm run test:qamrov faqat mantiq va komponent loyihalarini o'lchaydi. Browser Mode'da qamrov (@vitest/coverage-v8 brauzerda) — alohida sozlash va alohida o'lchov kerak; kanonga kirmadi. Halol raqam: qamrov 62.72 % (satrlar) — #11 dagidek.

5.2 CI

.github/workflows/test.yml ning brauzer job'iga bitta qadam:

yaml
      # 22/#12: story'lar — render, play, a11y (axe)
      # — o'sha Chrome'da
      - run: npm run test:storybook

actionlint — 0 xato. Playwright II dagidek: workflow yozilgan va tekshirilgan, lekin push qilinmagan va GitHub Actions'da ishlamagan. O'z repongizda birinchi push'dan keyin natijani o'zingiz ko'rasiz.

5.3 Narx

O'lchov Qiymat
O'rnatish +3 paket, 13 s
node_modules Storybook jami +58 MB (375 → 433 MB)
test:storybook 12 test, devor ~15 s
Ilova JS 206.7 KB — o'zgarmadi

@storybook/addon-a11y o'zi bilan axe-core 4.14.0 ni olib keldi. Accessibility testlari darsida Playwright uchun boshqa axe paketi qo'shiladi — o'shanda bitta loyihada ikki nusxa bo'ladi. Buni o'sha darsda o'lchaymiz.

6. Chromatic va jonli hujjat

6.1 Chromatic

Chromatic — Storybook mualliflari jamoasining bulut xizmati. Hujjatiga ko'ra: Storybook build'ini yuklaydi (jamoa havola orqali ko'radi), har story'ning suratini oladi va oldingisi bilan solishtiradi (visual test), o'zgarishni dizayner yoki ko'rib chiquvchi "qabul qilaman" deb belgilaydi. Paketlar: chromatic CLI (18.13.0) va @chromatic-com/storybook addon'i (5.6.0) — 2026-10 holatiga.

vazifalar da Chromatic ishlatilmagan: hisob kerak, va kursda hisob ochmaymiz. Suratlarni solishtirish g'oyasini keyingi darsda Playwright bilan, o'z mashinamizda qilamiz — Chromatic xuddi shu ishni bulutda va story'lar darajasida qiladi.

6.2 Dizayn tizimi hujjatlari

Story'lar, autodocs (Storybook I) va MDX sahifalar birga jonli hujjat bo'ladi. MDX — oddiy matn ichida jonli story'lar. Hujjatda: tokenlar sahifasi (dizayn tizimi), har komponentning variantlari, "qachon ishlatilmaydi" bo'limi. storybook build natijasi (statik sayt) jamoaning ichki manziliga qo'yiladi. Kod o'zgarsa, hujjat ham o'zgaradi — eskirgan PDF qo'llanmadan farqi shu.

Story'lardan RTL testlarida ham foydalanish mumkin: @storybook/react dagi composeStories story'ni oddiy komponentga aylantiradi (dekorator va args bilan) — jsdom'dagi *.test.tsx da render(<Primary />). Bu portativ story'lar (portable stories) deyiladi; vazifalar da ishlatilmadi — story'lar allaqachon Vitest'da test.

7. Vazifalar qadami: story'lar — testlar (holat 12)

7.1 Nima o'zgardi

Fayl O'zgarish
package.json 2 addon, test:storybook skripti
.storybook/main.ts addons
.storybook/preview.tsx parameters.a11y.test = "error"
vitest.config.ts storybook loyihasi
stories/* 3 ta play: Tugma/Bosiladi, OddiyQator/Belgilash, OchirishDialog/Tasdiq
test.yml brauzer job'ida test:storybook

OddiyQator/Belgilash — katakchani bosadi va fayl darajasidagi amallar.onAlmashtir josusini tekshiradi:

tsx
export const Belgilash: Story = {
  play: async ({ canvas, userEvent }) => {
    await userEvent.click(
      canvas.getByRole("checkbox", { name: "Non olish #bozor" }),
    );
    await expect(amallar.onAlmashtir).toHaveBeenCalledWith(1);
  },
};

args emas, amallar — josus kontekst orqali berilgan (Storybook I dagi dekorator), args'da emas.

7.2 Natija va commit

Testlar: Vitest 348, brauzer 6, storybook 12, e2e 10. Lint, format:check, tip, knip — toza. JS 206.7 KB.

bash
git add -A
git commit

Commit (kanon): test: story'lar Vitest testi — play funksiyalari va addon-a11y (axe).

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
expected "vi.fn()" to be called… darhol hodisa keyingi task'da await waitFor(() => expect(…))
Found multiple elements… Docs'da play da screen canvas
Interactions panelida qadamlar yo'q expect oldida await yo'q await expect(…)
listitem, button-name … axe qoidasi o'ramni yoki komponentni tuzating; vaqtincha "todo"
dekoratorlar testda ishlamaydi qo'lda setProjectAnnotations Storybook 10 da faylni olib tashlang
Executable doesn't exist Playwright brauzeri yo'q channel: "chrome" yoki npx playwright install chromium

9. Mashqlar

Mashqlar kurs/mashqlar/22/12-storybook-play/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.

1-mashq (oson): bo'sh joylarni to'ldiring

  • Story chizilgandan keyin ishlaydigan funksiya —
  • Faqat shu story ichidan qidiradigan so'rovlar —
  • Buzilish bo'lsa ham test o'tsin, faqat ogohlantirsin — a11y: { test: "" }
  • Story'larni testga aylantiradigan Vitest plagini —
  • Hodisa keyinroq keladi — await (() => expect(…))
Yechim
  • play — async ({ args, canvas, userEvent }) => { … }.
  • canvas — within(story elementi); screen butun sahifani ko'radi.
  • todo — "error" yiqitadi, "off" o'chiradi.
  • storybookTest — @storybook/addon-vitest/vitest-plugin dan.
  • waitFor — o'tguncha qayta urinadi (sukut 1 s).

2-mashq (o'rta): savatcha story'si uchun play

CartButton — «Savatga qo'shish» tugmasi; bosilganda onAdd(dishId) chaqiradi va matn «Savatda » ga o'zgaradi, tugma o'chiriladi. Meta: args: { dishId: 12, onAdd: vi.fn() }. AddsOnce story'sini yozing: play tugmani ikki marta bosadi (userEvent.dblClick emas — ikki click), keyin onAdd bir marta va 12 bilan chaqirilganini, tugma disabled ekanini tekshiradi. «Story + play» bo'limidagi runStory bilan yurgizing.

Ishora: o'chirilgan tugmani user-event bosmaydi — ikkinchi click hech narsa qilmaydi; toHaveBeenCalledExactlyOnceWith (Vitest II).

Yechim
jsx
import { render, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { expect, test, vi } from "vitest";

function CartButton({ dishId, onAdd }) {
  const [added, setAdded] = useState(false);
  const add = () => {
    setAdded(true);
    onAdd(dishId);
  };
  return (
    <button onClick={add} disabled={added}>
      {added ? "Savatda ✓" : "Savatga qo'shish"}
    </button>
  );
}

const meta = {
  component: CartButton,
  args: { dishId: 12, onAdd: vi.fn() },
};

const AddsOnce = {
  play: async ({ args, canvas, userEvent }) => {
    const btn = canvas.getByRole("button", {
      name: "Savatga qo'shish",
    });
    await userEvent.click(btn);
    await userEvent.click(btn);
    await expect(args.onAdd).toHaveBeenCalledExactlyOnceWith(12);
    await expect(btn).toBeDisabled();
    console.log(btn.textContent);
  },
};

async function runStory(meta, story) {
  const args = { ...meta.args, ...story.args };
  const Component = meta.component;
  const { container } = render(<Component {...args} />);
  await story.play?.({
    args,
    canvas: within(container),
    userEvent: userEvent.setup(),
  });
}

test("AddsOnce", () => runStory(meta, AddsOnce));

Konsolda:

text
Savatda ✓
  ✓ AddsOnce
Testlar: 1 o'tdi (jami 1)

btn o'zgaruvchisi bosishdan keyin ham o'sha element — React matnni yangiladi, elementni almashtirmadi. Shuning uchun nom o'zgargan bo'lsa ham toBeDisabled uni tekshira oladi.

3-mashq (qiyin): kichik a11y tekshiruvchi

Story'lar uchun axe'ning ikki qoidasini o'zingiz yozing — checkA11y(container) funksiyasi buzilishlar massivini qaytarsin: (1) button-name — nomsiz tugma; (2) listitem — ota-onasi <ul>/<ol> bo'lmagan <li>. Keyin ikki story bilan sinang: Broken (nomsiz tugma va yalang'och <li>) — 2 buzilish; Fixed — 0. Har buzilishni qoida: element ko'rinishida konsolga chiqaring.

Ishora: tugma nomini RTL'ning o'zi hisoblaydi — computeAccessibleName @testing-library/dom ichida, lekin eksport qilinmagan; oddiyroq yo'l — btn.textContent.trim(), aria-label va aria-labelledby ni tekshirish. <li> uchun li.parentElement.tagName.

Yechim
jsx
import { render } from "@testing-library/react";
import { expect, test } from "vitest";

function checkA11y(container) {
  const problems = [];
  for (const btn of container.querySelectorAll("button")) {
    const named =
      btn.textContent.trim() !== "" ||
      btn.getAttribute("aria-label") ||
      btn.getAttribute("aria-labelledby");
    if (!named) problems.push(`button-name: ${btn.outerHTML}`);
  }
  for (const li of container.querySelectorAll("li")) {
    const parent = li.parentElement.tagName;
    if (parent !== "UL" && parent !== "OL") {
      problems.push(`listitem: ${li.outerHTML}`);
    }
  }
  return problems;
}

function Broken() {
  return (
    <div>
      <li>Osh</li>
      <button>
        <svg aria-hidden="true" width="16" height="16" />
      </button>
    </div>
  );
}

function Fixed() {
  return (
    <div>
      <ul>
        <li>Osh</li>
      </ul>
      <button aria-label="Oshni o'chirish">
        <svg aria-hidden="true" width="16" height="16" />
      </button>
    </div>
  );
}

test("Broken — 2 buzilish", () => {
  const { container } = render(<Broken />);
  const problems = checkA11y(container);
  for (const p of problems) console.log(p);
  expect(problems).toHaveLength(2);
});

test("Fixed — 0", () => {
  const { container } = render(<Fixed />);
  expect(checkA11y(container)).toEqual([]);
});

Konsolda:

text
button-name: <button><svg aria-hidden="true" width="16" height="16"></svg></button>
listitem: <li>Osh</li>
  ✓ Broken — 2 buzilish
  ✓ Fixed — 0
Testlar: 2 o'tdi (jami 2)

Bu yerda querySelectorAll — to'g'ri tanlov: biz foydalanuvchi oqimini emas, DOM tuzilmasini tekshiryapmiz (axe ham shunday qiladi). Haqiqiy axe ancha aqlli: title, <label>, role="presentation" ni ham hisobga oladi, aria-labelledby havolasi bo'sh emasligini tekshiradi. O'z tekshiruvchingizni loyihada ishlatmang — bu mashq axe ichini tushunish uchun.

4-mashq: Vazifalar qadami

test/storybook-vitest branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling: ikki addon, storybook loyihasi, a11y.test, uch play, CI qadami.

Yechim

Tekshiruv ro'yxati:

  • npm run test:storybook — 4 fayl, 12 test.
  • Tasdiq dagi waitFor ni olib tashlang — test yiqiladi (onJavob hali chaqirilmagan). Qaytaring.
  • VazifaRoyxati story o'ramidagi <ul> ni olib tashlang — UchVazifa listitem bilan yiqiladi. Qaytaring.
  • Vaqtincha matnsiz tugma story'si — button-name; keyin faylni o'chiring.
  • npx storybook dev — Tugma/Bosiladi da Interactions panelida qadamlar ko'rinadi.

10. Real ishda

  • Dizayn tizimi jamoalari story'larni asosiy test birligi qiladi: komponent kutubxonasida har variant — story, har story — test va a11y tekshiruvi.
  • Pull request'da story testlari CI'da yuradi; Chromatic kabi xizmatlar esa suratlar farqini ko'rib chiquvchiga ko'rsatadi.
  • "Yozilgan story — bepul test": jamoalar ko'pincha yangi komponentga avval story yozadi, play ni keyin qo'shadi — tutun testi birinchi kundan bor.
  • Intervyu savollari: "play funksiyasi nima?", "story testi va RTL testining farqi?", "axe nimani ushlamaydi?" (klaviatura oqimi, mazmunli matn — Accessibility testlari).

Xulosa

  • play — story chizilgach ishlaydigan test: canvas, userEvent, args, expect (storybook/test); play siz story — tutun testi.
  • Hodisa keyingi task'da kelsa — waitFor; canvas ni screen o'rniga ishlating.
  • @storybook/addon-vitest (storybookTest) har story'ni Browser Mode testiga aylantiradi: vazifalar da 12 test, ~15 s.
  • @storybook/addon-a11y + a11y.test: "error" — axe buzilishi testni yiqitadi; "todo" — bosqichma-bosqich kiritish.
  • Birinchi o'tish 4 router xatosi, 1 waitFor va 1 a11y topilmasini ko'rsatdi — story'lar ochilmaguncha xato yashirin edi.
  • Chromatic — bulutdagi visual test va hujjat xizmati (hisob kerak, kanonda yo'q).

Keyingi dars: Visual regression testlar — sahifa suratini oltin rasm bilan piksel bo'yicha solishtirish va vazifalar dagi 20-qismdan beri yashirin turgan xatoni topish.

Manbalar

  • Storybook: Interaction testing (play, canvas, step, mount), Accessibility testing (parameters.a11y.test), Vitest addon (storybook.js.org/docs/writing-tests)
  • Storybook MIGRATION.md — storybook/test, setProjectAnnotations, essentials yadroga ko'chgani
  • axe-core qoidalari: dequeuniversity.com/rules/axe/4.14 (button-name, listitem)
  • Chromatic — chromatic.com/docs (hujjat bo'yicha; kanonda hisob ochilmagan)
  • Haqiqiy chiqishlar: Vitest 5.0.4, Storybook 10.6.1, Chrome 155, Node 24.21.0 (2026-10-20)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Storybook II: play funksiyasi, a11y addon va Vitest bilan story testlari — IlmHamroh