Jadual Kandungan
Mulakan ujian reaksi
Pilihan 1: Ujian Unit
Pilihan 2: Ujian Integrasi
Jadi, apa yang diperlukan untuk ujian unit?
Faedah lain
Jelas Waitfor Block
Dalam talian ia memberi komen
Langkah seterusnya untuk pasukan
Rumah hujung hadapan web tutorial css Ujian Integrasi React: Liputan yang lebih besar, ujian yang lebih sedikit

Ujian Integrasi React: Liputan yang lebih besar, ujian yang lebih sedikit

Apr 07, 2025 am 09:20 AM

Ujian Integrasi React: Liputan yang lebih besar, ujian yang lebih sedikit

Untuk laman web interaktif seperti yang dibina dengan React, ujian integrasi adalah pilihan semula jadi. Mereka mengesahkan bagaimana pengguna berinteraksi dengan aplikasi tanpa overhead tambahan ujian akhir-ke-akhir.

Artikel ini menggambarkannya dengan latihan yang bermula dengan laman web yang mudah, menggunakan ujian unit dan ujian integrasi untuk mengesahkan tingkah laku, dan menunjukkan bagaimana ujian integrasi dapat mencapai nilai yang lebih besar dengan lebih sedikit kod kod. Artikel ini menganggap bahawa anda sudah biasa dengan ujian dalam React dan JavaScript. Kebiasaan dengan perpustakaan ujian JEST dan React boleh membantu, tetapi tidak diperlukan.

Terdapat tiga jenis ujian:

  • Ujian unit secara bebas mengesahkan sekeping kod. Mereka mudah menulis, tetapi mungkin mengabaikan gambaran besar.
  • Ujian akhir-ke-akhir (E2E) menggunakan rangka kerja automatik seperti cypress atau selenium untuk berinteraksi dengan laman web anda seperti pengguna: memuatkan halaman, mengisi borang, mengklik butang, dan banyak lagi. Mereka biasanya ditulis dan berjalan lebih perlahan, tetapi sangat dekat dengan pengalaman pengguna yang sebenar.
  • Ujian integrasi adalah di antara. Mereka mengesahkan bagaimana pelbagai unit aplikasi berfungsi bersama -sama, tetapi lebih ringan daripada ujian E2E. Sebagai contoh, Jest datang dengan beberapa utiliti terbina dalam untuk memudahkan ujian integrasi; JEST menggunakan JSDOM di latar belakang untuk mensimulasikan API penyemak imbas biasa, dengan kurang overhead daripada automasi, dan alat mengejek yang kuat dapat mensimulasikan panggilan API luaran.

Satu lagi perkara yang perlu diperhatikan: Dalam aplikasi React, ujian unit dan ujian integrasi ditulis dengan cara yang sama, dan alat digunakan .

Mulakan ujian reaksi

Saya membuat aplikasi React mudah (tersedia di GitHub) dengan borang log masuk. Saya menghubungkannya dengan Reqres.in, yang merupakan API yang berguna yang saya dapati untuk menguji projek-projek front-end.

Anda boleh log masuk dengan jayanya:

... atau menemui mesej ralat dari API:

Struktur kod adalah seperti berikut:

 <code>LoginModule/ ├── components/ │ ├── Login.js // 渲染LoginForm、错误消息和登录确认│ └── LoginForm.js // 渲染登录表单字段和按钮├── hooks/ │ └── useLogin.js // 连接到API 并管理状态└── index.js // 将所有内容整合在一起</code>
Salin selepas log masuk

Pilihan 1: Ujian Unit

Jika anda suka menulis ujian seperti saya -mungkin memakai fon kepala dan bermain muzik yang bagus di Spotify -maka anda mungkin tidak dapat menahan ujian unit menulis untuk setiap fail.

Walaupun anda bukan peminat ujian, anda mungkin mengusahakan projek yang "cuba melakukan kerja yang baik untuk menguji" tanpa strategi yang jelas, dan kaedah ujian adalah "Saya fikir setiap fail harus mempunyai ujian sendiri?"

Ini kelihatan seperti ini (saya menambah unit dalam nama fail ujian untuk kejelasan):

 <code>LoginModule/ ├── components/ │  ├── Login.js │  ├── Login.unit.test.js │  ├── LoginForm.js │  └── LoginForm.unit.test.js ├── hooks/ │  ├── useLogin.js │  └── useLogin.unit.test.js ├── index.js └── index.unit.test.js</code>
Salin selepas log masuk

Saya menyelesaikan latihan di GitHub untuk menambah semua ujian unit ini dan mencipta ujian: Liputan: Skrip unit untuk menjana laporan liputan (ciri terbina dalam JEST). Kami boleh mencapai liputan 100% melalui empat fail ujian unit:

Liputan 100% biasanya luar biasa, tetapi mungkin untuk asas kod mudah seperti itu.

Mari menggali salah satu ujian unit yang dibuat untuk cangkuk React Onlogin. Jika anda tidak biasa dengan cangkuk React atau bagaimana untuk menguji mereka, jangan risau.

 ujian ('aliran log masuk yang berjaya', async () => {
 // Simulasi Jest Response API yang berjaya
  .spyon (tingkap, 'ambil')
  .mockResolvedValue ({json: () => ({token: '123'})});

 const {result, WaitForRextUpdate} = renderHook (() => uselogin ());

 bertindak (() => {
  result.current.onsubmit ({
   E -mel: '[dilindungi e -mel]',
   Kata Laluan: 'Kata Laluan',
  });
 });

 // Tetapkan status untuk belum selesai
 menjangkakan (result.current.state) .toequal ({
  Status: 'menunggu',
  Pengguna: null,
  Ralat: null,
 });

 menunggu WaitForNextUpdate ();

 // Tetapkan status untuk diselesaikan dan simpan alamat e -mel yang diharapkan (result.current.state) .toequal ({
  Status: 'diselesaikan',
  Pengguna: {
   E -mel: '[dilindungi e -mel]',
  },
  Ralat: null,
 });
});
Salin selepas log masuk

Ujian ini menarik untuk ditulis (kerana Perpustakaan Ujian Hooks React membuat ujian cangkuk mudah), tetapi ia mempunyai beberapa masalah.

Pertama, pengesahan ujian keadaan dalaman berubah dari 'menunggu' kepada 'diselesaikan'; Butiran pelaksanaan ini tidak terdedah kepada pengguna, jadi ia mungkin bukan perkara yang baik untuk diuji. Jika kita refactor permohonan, kita perlu mengemas kini ujian ini, walaupun tiada perubahan dari perspektif pengguna.

Juga, sebagai ujian unit, ini hanya sebahagian daripadanya. Jika kami ingin mengesahkan ciri -ciri lain dalam proses log masuk, seperti menukar teks butang penyerahan untuk "memuatkan", kita perlu melakukannya dalam fail ujian yang berbeza.

Pilihan 2: Ujian Integrasi

Mari kita pertimbangkan untuk menambah alternatif kepada ujian integrasi untuk mengesahkan proses ini:

 <code>LoginModule/ ├── components/ │  ├── Login.js │  └── LoginForm.js ├── hooks/ │  └── useLogin.js ├── index.js └── index.integration.test.js</code>
Salin selepas log masuk

Saya melaksanakan ujian ini dan ujian: Liputan: Skrip Integrasi untuk Menjana Laporan Liputan. Sama seperti ujian unit, kita boleh mencapai liputan 100%, tetapi kali ini semuanya dalam satu fail dan memerlukan lebih sedikit baris kod.

Berikut adalah ujian integrasi yang meliputi proses log masuk yang berjaya:

 Ujian ('Log masuk yang berjaya', async () => {
  Jest
    .spyon (tingkap, 'ambil')
    .mockResolvedValue ({json: () => ({token: '123'})});

  membuat (<loginmodule></loginmodule> );

  const emailField = Screen.getByRole ('TextBox', {name: 'Email'});
  const passwordField = screen.getByLabelText ('kata laluan');
  butang const = screen.getByRole ('butang');

  // isi dan serahkan borang FireEvent.Change (EmailField, {Target: {value: '[E -mel dilindungi]'}});
  fireEvent.change (passwordField, {target: {value: 'password'}});
  FireEvent.Click (butang);

  // ia menetapkan keadaan beban yang diharapkan (butang) .tobedisabled ();
  menjangkakan (butang) .toHaveTextContent ('Loading ...');

  menunggu menunggu untuk (() => {
    // Ia menyembunyikan unsur bentuk yang diharapkan (butang) .not.tobeIntHedocument ();
    menjangkakan (emailField) .not.tobeIntHedOcument ();
    menjangkakan (kata laluan) .not.tobeIntHedocument ();

    // ia memaparkan teks kejayaan dan alamat e -mel const loggedIntext = screen.getByText ('log masuk sebagai');
    menjangkakan (loggedIntext) .tobeIntHedocument ();
    const emailAddressText = Screen.getByText ('[E -mel dilindungi]');
    menjangkakan (e -melAddressText) .TobeIntHedocument ();
  });
});
Salin selepas log masuk

Saya sangat menyukai ujian ini kerana ia mengesahkan keseluruhan proses log masuk dari perspektif pengguna: bentuk, status beban dan mesej pengesahan yang berjaya. Ujian integrasi sangat baik untuk aplikasi React, tepat kerana kes penggunaan ini; Pengalaman pengguna adalah apa yang ingin kami uji, dan ini hampir selalu melibatkan pelbagai coretan kod yang bekerja bersama -sama .

Ujian ini tidak memahami komponen atau cangkuk yang membuat kerja tingkah laku yang diharapkan, yang bagus. Selagi pengalaman pengguna tetap sama, kita boleh menulis semula dan refactor butiran pelaksanaan ini tanpa melanggar ujian.

Saya tidak akan menggali keadaan awal proses log masuk dan ujian integrasi lain untuk pengendalian ralat, tetapi saya menggalakkan anda untuk melihatnya di GitHub.

Jadi, apa yang diperlukan untuk ujian unit?

Daripada mempertimbangkan ujian unit berbanding ujian integrasi, mari kita mengambil langkah mundur dan berfikir tentang bagaimana kita memutuskan apa yang kita perlukan untuk menguji di tempat pertama. LoginModule perlu diuji kerana ia adalah entiti yang kami mahukan pengguna (fail lain dalam aplikasi) untuk dapat digunakan dengan keyakinan.

Sebaliknya, tidak perlu menguji cangkuk onlogin, kerana ia hanya butiran pelaksanaan loginmodule. Walau bagaimanapun, jika keperluan kami berubah dan OnLogin telah menggunakan kes -kes di tempat lain, kami perlu menambah ujian kami sendiri (unit) untuk mengesahkan fungsinya sebagai utiliti yang boleh diguna semula. (Kami juga perlu memindahkan fail kerana ia tidak lagi khusus untuk loginmodule.)

Ujian unit masih mempunyai banyak kes penggunaan, seperti keperluan untuk mengesahkan pemilih, cangkuk, dan fungsi biasa yang boleh diguna semula. Apabila membangunkan kod anda, anda juga dapat membantu menggunakan pembangunan Ujian Unit, walaupun anda memindahkan logik itu sehingga ujian integrasi kemudian.

Di samping itu, ujian unit melakukan tugas yang baik untuk ujian menyeluruh untuk pelbagai input dan kes penggunaan. Sebagai contoh, jika borang saya perlu menunjukkan pengesahan sebaris untuk pelbagai senario (mis. E -mel tidak sah, kata laluan yang hilang, kata laluan yang terlalu pendek), saya akan merangkumi kes wakil dalam ujian integrasi dan kemudian menggali kes tertentu dalam ujian unit.

Faedah lain

Sekarang kita berada di sini, saya ingin bercakap tentang beberapa tip sintaks yang membantu memastikan ujian integrasi saya jelas dan teratur.

Jelas Waitfor Block

Ujian kami perlu mempertimbangkan latensi antara keadaan pemuatan dan keadaan yang berjaya login:

 butang const = screen.getByRole ('butang');
FireEvent.Click (butang);

menjangkakan (butang) .not.tobeIntHedocument (); // Terlalu cepat, butang masih ada!
Salin selepas log masuk

Kita boleh melakukan ini menggunakan fungsi pembantu Waitfor Perpustakaan Ujian DOM:

 butang const = screen.getByRole ('butang');
FireEvent.Click (butang);

menunggu menunggu untuk (() => {
 menjangkakan (butang) .not.tobeIntHedocument (); // ah, lebih baik});
Salin selepas log masuk

Tetapi bagaimana jika kita mahu menguji beberapa projek lain? Tidak banyak contoh yang baik di internet tentang cara mengendalikan ini, dan dalam projek -projek yang lalu saya telah meletakkan projek lain di luar Waitfor:

 // tunggu butang menunggu menunggu untuk (() => {
 menjangkakan (butang) .not.tobeIntHedocument ();
});

// kemudian menguji mesej pengesahan const vactionText = getByText ('log masuk sebagai [dilindungi e -mel]');
menjangkakan (pengesahanText) .tobeIntHedocument ();
Salin selepas log masuk

Ini berfungsi, tetapi saya tidak menyukainya kerana ia menjadikan keadaan butang kelihatan istimewa, walaupun kita dapat dengan mudah menukar urutan pernyataan ini:

 // tunggu mesej pengesahan menunggu menunggu untuk (() => {
 const pengesahanText = getByText ('log masuk sebagai [dilindungi e -mel]');
 menjangkakan (pengesahanText) .tobeIntHedocument ();
});

// kemudian menguji butang mengharapkan (butang) .not.tobeIntHedocument ();
Salin selepas log masuk

Nampaknya saya lebih baik untuk mengumpulkan segala yang berkaitan dengan kemas kini yang sama ke dalam panggilan balik menunggu:

 menunggu menunggu untuk (() => {
 menjangkakan (butang) .not.tobeIntHedocument ();

 const pengesahanText = screen.getByText ('log masuk sebagai [dilindungi e -mel]');
 menjangkakan (pengesahanText) .tobeIntHedocument ();
});
Salin selepas log masuk

Saya sangat menyukai teknik ini untuk pernyataan mudah seperti ini, tetapi dalam beberapa kes ia dapat melambatkan ujian, menunggu kegagalan yang berlaku dengan segera di luar Waitfor. Untuk contoh ini, lihat "Multiple Pernyataan dalam satu panggilan Waitfor" dalam kesilapan biasa Perpustakaan Ujian React.

Untuk ujian yang mengandungi beberapa langkah, kita boleh menggunakan pelbagai tunggu untuk berturut -turut:

 butang const = screen.getByRole ('butang');
const emailField = Screen.getByRole ('TextBox', {name: 'Email'});

// isi borang fireEvent.change (emailField, {target: {value: '[e -mel dilindungi]'}});

menunggu menunggu untuk (() => {
 // Periksa sama ada butang diaktifkan jangkaan (butang) .not.tobedisabled ();
  menjangkakan (butang) .thaveTextContent ('hantar');
});

// menyerahkan borang fireEvent.click (butang);

menunggu menunggu untuk (() => {
 // Periksa sama ada butang tidak lagi wujud (butang) .not.tobeIntHedOcument ();
});
Salin selepas log masuk

Jika anda hanya menunggu satu item muncul, anda boleh menggunakan pertanyaan Findby. Ia menggunakan Waitfor di latar belakang.

Dalam talian ia memberi komen

Satu lagi ujian amalan terbaik ialah menulis lebih sedikit, ujian yang lebih panjang; Ini membolehkan anda mengaitkan kes ujian dengan proses pengguna yang penting sambil mengekalkan ujian yang diasingkan untuk mengelakkan tingkah laku yang tidak dijangka. Saya bersetuju dengan pendekatan ini, tetapi ia boleh menimbulkan cabaran dalam menjaga kod yang dianjurkan dan mendokumentasikan tingkah laku yang diperlukan. Kami memerlukan pemaju masa depan untuk dapat kembali ke ujian dan memahami apa yang dilakukannya, mengapa ia gagal, dll.

Sebagai contoh, katakan salah satu daripada jangkaan ini mula gagal:

 ia ('mengendalikan aliran log masuk yang berjaya', async () => {
 // Sembunyikan permulaan ujian untuk kejelasan

  mengharapkan (butang) .tobedisabled ();
  menjangkakan (butang) .toHaveTextContent ('Loading ...');


 menunggu menunggu untuk (() => {
  menjangkakan (butang) .not.tobeIntHedocument ();
  menjangkakan (emailField) .not.tobeIntHedOcument ();
  menjangkakan (kata laluan) .not.tobeIntHedocument ();


  const pengesahanText = screen.getByText ('log masuk sebagai [dilindungi e -mel]');
  menjangkakan (pengesahanText) .tobeIntHedocument ();
 });
});
Salin selepas log masuk

Pemaju yang melihat kandungan ini tidak dapat dengan mudah menentukan apa yang sedang diuji, dan mungkin sukar untuk menentukan sama ada kegagalan adalah bug (yang bermaksud kita harus menetapkan kod) atau perubahan tingkah laku (yang bermaksud kita harus menetapkan ujian).

Penyelesaian kegemaran saya ialah menggunakan sintaks ujian yang kurang dikenali untuk setiap ujian dan menambah komen gaya sebaris yang menerangkan setiap tingkah laku utama yang diuji:

 Ujian ('Log masuk yang berjaya', async () => {
 // Sembunyikan permulaan ujian untuk kejelasan

 // Ia menetapkan status pemuatan yang diharapkan (butang) .tobedisabled ();
  menjangkakan (butang) .toHaveTextContent ('Loading ...');


 menunggu menunggu untuk (() => {
  // Ia menyembunyikan unsur bentuk yang diharapkan (butang) .not.tobeIntHedocument ();
  menjangkakan (emailField) .not.tobeIntHedOcument ();
  menjangkakan (kata laluan) .not.tobeIntHedocument ();


  // Ia memaparkan teks kejayaan dan alamat e -mel const vactionText = screen.getByText ('log masuk sebagai [dilindungi e -mel]');
  menjangkakan (pengesahanText) .tobeIntHedocument ();
 });
});
Salin selepas log masuk

Komen -komen ini tidak secara ajaib diintegrasikan dengan Jest, jadi jika anda mengalami kegagalan, nama ujian yang gagal akan sesuai dengan parameter yang anda lulus ke tag ujian, dalam hal ini "Login Berjaya". Walau bagaimanapun, mesej ralat Jest mengandungi kod sekitarnya, jadi komen IT ini masih membantu mengenal pasti tingkah laku yang gagal. Apabila saya mengeluarkan bukan dari jangkaan, saya mendapat mesej ralat berikut:

Untuk mendapatkan kesilapan yang lebih jelas, terdapat pakej yang dipanggil jestan-expect-message yang membolehkan anda menentukan mesej ralat untuk setiap jangkaan:

 mengharapkan (butang, 'butang masih dalam dokumen'). not.tobeIntHedocument ();
Salin selepas log masuk

Sesetengah pemaju lebih suka pendekatan ini, tetapi saya mendapati ia terlalu berbutir dalam kebanyakan kes, kerana ia biasanya melibatkan pelbagai jangkaan.

Langkah seterusnya untuk pasukan

Kadang -kadang saya harap kita dapat membuat peraturan linter untuk manusia. Jika ya, kami boleh menetapkan peraturan ujian pilihan untuk pasukan kami dan ia berakhir.

Tetapi, malangnya, kita perlu mencari penyelesaian yang lebih serupa untuk menggalakkan pemaju memilih ujian integrasi dalam beberapa kes, seperti contoh loginmodul yang kami diperkenalkan sebelum ini. Seperti kebanyakan perkara, ia datang kepada pasukan yang membincangkan strategi ujian anda, bersetuju dengan apa yang masuk akal untuk projek itu, dan -semoga ia mendokumentasikannya di ADR.

Apabila membangunkan pelan ujian, kita harus mengelakkan budaya yang memaksa pemaju menulis ujian untuk setiap fail. Pemaju perlu membuat keputusan ujian yang dimaklumkan dengan keyakinan tanpa perlu risau tentang "usaha" mereka. Laporan liputan Jest dapat membantu menyelesaikan masalah ini dengan menyediakan pemeriksaan kewarasan, walaupun ujian digabungkan di peringkat integrasi.

Saya masih tidak menganggap diri saya pakar ujian integrasi, tetapi melakukan latihan ini membantu saya memecahkan kes penggunaan di mana ujian integrasi menawarkan lebih banyak nilai daripada ujian unit. Saya berharap bahawa berkongsi ini dengan pasukan anda, atau melakukan latihan yang sama di pangkalan kod anda akan membantu membimbing anda menggabungkan ujian integrasi ke dalam aliran kerja anda.

Atas ialah kandungan terperinci Ujian Integrasi React: Liputan yang lebih besar, ujian yang lebih sedikit. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn

Alat AI Hot

Undresser.AI Undress

Undresser.AI Undress

Apl berkuasa AI untuk mencipta foto bogel yang realistik

AI Clothes Remover

AI Clothes Remover

Alat AI dalam talian untuk mengeluarkan pakaian daripada foto.

Undress AI Tool

Undress AI Tool

Gambar buka pakaian secara percuma

Clothoff.io

Clothoff.io

Penyingkiran pakaian AI

Video Face Swap

Video Face Swap

Tukar muka dalam mana-mana video dengan mudah menggunakan alat tukar muka AI percuma kami!

Alat panas

Notepad++7.3.1

Notepad++7.3.1

Editor kod yang mudah digunakan dan percuma

SublimeText3 versi Cina

SublimeText3 versi Cina

Versi Cina, sangat mudah digunakan

Hantar Studio 13.0.1

Hantar Studio 13.0.1

Persekitaran pembangunan bersepadu PHP yang berkuasa

Dreamweaver CS6

Dreamweaver CS6

Alat pembangunan web visual

SublimeText3 versi Mac

SublimeText3 versi Mac

Perisian penyuntingan kod peringkat Tuhan (SublimeText3)

Vue 3 Vue 3 Apr 02, 2025 pm 06:32 PM

Ia &#039; s! Tahniah kepada pasukan Vue untuk menyelesaikannya, saya tahu ia adalah usaha besar dan lama datang. Semua dokumen baru juga.

Sedikit di CI/CD Sedikit di CI/CD Apr 02, 2025 pm 06:21 PM

Saya &#039;

Bolehkah anda mendapatkan nilai harta CSS yang sah dari penyemak imbas? Bolehkah anda mendapatkan nilai harta CSS yang sah dari penyemak imbas? Apr 02, 2025 pm 06:17 PM

Saya mempunyai seseorang yang menulis dengan soalan yang sangat legit ini. Lea hanya blog tentang bagaimana anda boleh mendapatkan sifat CSS yang sah dari penyemak imbas. That &#039; s seperti ini.

Kad yang disusun dengan kedudukan melekit dan sasaran sass Kad yang disusun dengan kedudukan melekit dan sasaran sass Apr 03, 2025 am 10:30 AM

Pada hari yang lain, saya melihat sedikit ini sangat indah dari laman web Corey Ginnivan di mana koleksi kad timbunan di atas satu sama lain semasa anda menatal.

Menggunakan Markdown dan Penyetempatan di Editor Blok WordPress Menggunakan Markdown dan Penyetempatan di Editor Blok WordPress Apr 02, 2025 am 04:27 AM

Jika kita perlu menunjukkan dokumentasi kepada pengguna secara langsung dalam editor WordPress, apakah cara terbaik untuk melakukannya?

Membandingkan penyemak imbas untuk reka bentuk responsif Membandingkan penyemak imbas untuk reka bentuk responsif Apr 02, 2025 pm 06:25 PM

Terdapat beberapa aplikasi desktop ini di mana matlamat menunjukkan laman web anda pada dimensi yang berbeza pada masa yang sama. Oleh itu, anda boleh menulis

Kenapa kawasan -kawasan yang dikurangkan ungu di susun atur flex tersilap dianggap sebagai 'ruang limpahan'? Kenapa kawasan -kawasan yang dikurangkan ungu di susun atur flex tersilap dianggap sebagai 'ruang limpahan'? Apr 05, 2025 pm 05:51 PM

Soalan mengenai kawasan slash ungu dalam susun atur flex Apabila menggunakan susun atur flex, anda mungkin menghadapi beberapa fenomena yang mengelirukan, seperti dalam alat pemaju (D ...

Cara menggunakan grid CSS untuk tajuk dan kaki melekit Cara menggunakan grid CSS untuk tajuk dan kaki melekit Apr 02, 2025 pm 06:29 PM

CSS Grid adalah koleksi sifat yang direka untuk menjadikan susun atur lebih mudah daripada yang pernah berlaku. Seperti apa -apa, ada sedikit keluk pembelajaran, tetapi grid adalah

See all articles