הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

היום למדתי: הקשר בין globals לניקוי ה DOM ב vitest

יש ימים בהם מנגנון שבכלל לא חשבת עליו פתאום מחליט להזכיר לך שהוא דווקא די חשוב. כך קרה לי היום עם מנגנון הזרקת הגלובאליים של vitest. המנגנון בגדול אומר שאם תדליקו מתג בשם globals בקובץ הקונפיגורציה בצורה כזאת:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: "jsdom",
  },
})

אז בכל קבצי הבדיקות שלכם תוכלו להסתמך על זה שיש describe, it ו expect ואולי עוד כמה משתנים גלובאליים. ברירת המחדל היא false ואז צריך לייבא הכל לבד עם שורה כזאת בתחילת קובץ בדיקות:

import { describe, it, expect } from 'vitest';

עד לפה אין סיבה להתרגש, אבל מסתבר שלהזרקת ה globals יש עוד אפקט והוא הרבה יותר מורגש: בעת בדיקת קומפוננטות ריאקט עם react-testing-library, הספריה תנקה את ה DOM בין בדיקה לבדיקה אם ה globals מוזרקים, אבל לא תנקה בלעדיהם.

במילים אחרות בקובץ בדיקות כזה:

import { describe, it, expect } from 'vitest';
import { screen, render } from '@testing-library/react';
import App from './App';

describe('App Changes Colors', () => {
  it('shows a button', () => {
    render(<App />);
    const btn = screen.getByRole('button', { name: '0' });
    expect(btn).toBeTruthy();
  });
  it('shows a button', () => {
    render(<App />);
    const btn = screen.getByRole('button', { name: '0' });
    expect(btn).toBeTruthy();
  });
});

אם יש הזרקת גלובאליים הכל עובד, אבל בלעדיה הבדיקה השניה נכשלת כי getByRole תקוע עם שני כפתורים על המסך, אחד מהבדיקה שהוא יצר והשני מהבדיקה הראשונה שלא נמחק.

הפונקציה הרלוונטית מ testing-library שמטפלת בסיפור המחיקה נקראת cleanup וזה מה שמוסבר גם בתיעוד שלה:

    Please note that this is done automatically if the testing framework you're using supports the afterEach global and it is injected to your testing environment (like mocha, Jest, and Jasmine). If not, you will need to do manual cleanups after each test.

לכן אם אתם עובדים ב vitest בלי הזרקת גלובאליים תצטרכו לזכור להפעיל את cleanup בעצמכם. אבל יותר קל פשוט להדליק את הזרקת הגלובאליים ולא לחשוב על זה (עד העקיצה הבאה).

טיפ פלקסבוקס: המאפיין width על הילדים

דמיינו שניה דף HTML שמחולק לתיבת צד ותוכן מרכזי. נו, אתם מכירים כאלה, משהו כזה:

<div class="main">
  <div class="sidebar" >
    <ul>
      <li>hello world</li>
      <li>hello world</li>
      <li>hello world</li>
    </ul>

  </div>
  <div class="content">
    <p>
      Flexbox is really cool if you know how to use it
    </p>
  </div>
</div>

עם הגדרות ה CSS הבאות אפשר לקבוע שהרשימה בתיבת הצד תיקח 300 פיקסלים והתוכן המרכזי יקבל את כל השאר:

.main {
  display: flex;
}

.sidebar {
  width: 300px;
}

.content {
  flex: 1;
}

אבל אל תנסו את זה בבית.

המאפיין width של תיבת הצד הוא רק חלק מהסיפור. נכון, ב HTML הדוגמה הוא באמת יקבע את הרוחב, אבל זה רק בגלל שהדפדפן לא מצא סיבה טובה לכווץ אותו קצת. אם נעדכן את ה HTML ונוסיף תמונה רחבה:

<div class="main">
  <div class="sidebar" >
    <ul>
      <li>hello world</li>
      <li>hello world</li>
      <li>hello world</li>      
    </ul>

  </div>
  <div class="content">
    <p>
      Flexbox is really cool if you know how to use it
    </p>
    <div>
      <img src="https://placekitten.com/400/300" />
    </div>
  </div>
</div>

ועכשיו נשנה את גודל החלון נוכל לראות שבשלב מסוים הדפדפן מתחיל להילחץ - הוא לא רוצה לצייר פס גלילה אופקי בשביל החתול, ולכן הוא מוותר על חלק מהרוחב של תיבת הצד. הנה קודפן למשחקים:

הסיבה שהוא מרשה לעצמו ככה להשתולל היא שלא הגדרנו ערך למאפיין flex-shrink של תיבת הצד, וערך ברירת המחדל של מאפיין זה הוא 1 - שאומר שמותר לכווץ את האלמנט אם זה חוסך בעיות במקומות אחרים.

דרך קשוחה יותר לקבוע רוחב לילד בתוך מיכל פלקס היא לציין את flex-shrink להיות 0 (ועל הדרך גם את flex-grow):

.sidebar {
  width: 300px;
  flex-shrink: 0;
  flex-grow: 0;
}

הגדרה כזאת מכריחה את הדפדפן להשאיר את האלמנט ברוחב 300, גם אם הוא היה מעדיף אחרת.

ואם אנחנו כבר פה שווה להזכיר ש width הוא לא הדרך הכי טובה לקבוע גודל של ילד בתוך מיכל פלקס, בגלל שמיכל פלקס יכול גם להיות אנכי. המאפיין flex-basis יקבע את הרוחב או הגובה של האלמנט לפי כיוון המיכל ולכן הקוד הזה עדיף:

.sidebar {
  flex-basis: 300px;
  flex-shrink: 0;
  flex-grow: 0;
}

ולזה יש כבר קיצור דרך פלקסי שבטח פגשתם בכל מיני דוגמאות:

.sidebar {
  flex: 0 0 300px;
}

מדריך: איך לבדוק פרויקט vite עם vitest

לפני כמה חודשים העברתי כאן וובינר על ויט, כלי שורת פקודה לבניית פרויקטי ווב. לויט יש תפריטים יפים כדי ליצור פרויקטים מכל מיני סוגים (ריאקט, ויו, סבלט), אבל הוא גם תומך בספריות אחרות באמצעות קונפיגורציה למשל סוליד.

כשהעברתי את הוובינר vitest היה עדיין בתחילת הדרך ולא היתה דרך סטנדרטית לכתוב בדיקות לפרויקטי ויט. זה השתנה לאחרונה ו vitest מספק אינטגרציה טובה עם ויט, תחביר כמעט זהה למה שאנחנו מכירים מ jest ועוד כמה פיצ'רים מעניינים כמו הרצת בדיקות במקביל או כתיבת בדיקות בגוף הקוד.

במדריך זה אני אצור אתכם פרויקט react עם vite, אתקין את vitest ואכתוב את הבדיקה הראשונה לפרויקט. מוכנים? הנה זה בא.

המשך קריאה

טיפ JavaScript: שמירת מידע לקבצים וטעינה חזרה

יישומי Front End מאפשרים למשתמשים להכניס מידע. הרבה פעמים אנחנו שומרים את המידע בשרת אבל לפעמים אין לנו שרת או שהמידע לא מתאים לשמירה בבסיס הנתונים ואז היינו רוצים לתת את המידע למשתמש בתור קובץ. בדוגמה זו אראה איך לממש טופס עם שני כפתורים - כפתור אחד "מוריד" את המידע שבטופס לקובץ JSON על מחשב המשתמש וכפתור שני "מעלה" קובץ JSON מהמחשב כדי למלא ממנו את הטופס.

המשך קריאה

מדריך קוד: שליפה מבסיס נתונים ב Node.JS והצגת המידע כטבלא ב HTML

בפוסט זה אדגים איך לכתוב אפליקציית Node.JS ו Express פשוטה, ששולפת מידע מבסיס נתונים באמצעות ספריית Knex.JS ומציגה אותו בתור טבלה ב HTML. חסרי הסבלנות ביניכם מוזמנים לדלג על ההסבר ולקפוץ ישר לקוד במאגר: https://github.com/ynonp/node-knex-ejs-demo

לכל השאר - בואו נראה איך זה עובד.

המשך קריאה

טיפ CSS: המאפיין display: contents

שני מאפייני ה Layout המתקדמים של CSS, פלקסבוקס וגריד, כוללים מגבלה קצת מעצבנת: הם מכילים את הגדרות העיצוב על הילדים הבלוקים הישירים שלהם.

במילים אחרות אם יש לי פס עליון באתר ובו 6 לינקים, וה HTML שלי נראה ככה:

<div class="topbar">
    <a href="#">link #1</a>
    <a href="#">link #2</a>
    <a href="#">link #3</a>
    <a href="#">link #4</a>
    <a href="#">link #5</a>
    <a href="#">link #6</a>
</div>

אז אין בעיה להשתמש ב CSS כזה כדי לסדר את כל הלינקים בתוך פלקסבוקס ולחלק להם את המקום שווה בשווה:

.topbar {
  display: flex;
}

.topbar a {
  padding: 0.2rem;
  flex: 1;
}

אבל אם במקרה החלטתי לחלק את הלינקים ב HTML למספר "בלוקים", רק מבחינה סמנטית או כי היה לי יותר נוח לייצר HTML כזה:

<div class="topbar">
   <div class="part1">
    <a href="#">link #1</a>
    <a href="#">link #2</a>
    <a href="#">link #3</a>
  </div>

  <div class="part2">
    <a href="#">link #4</a>
    <a href="#">link #5</a>
  </div>

  <div class="part3">
    <a href="#">link #6</a>
  </div>
</div>

אז פתאום כל הפלקסבוקס שלי נשבר. יותר מדויק להגיד שהוא לא נשבר פשוט האלמנטים שמסודרים בפלקסבוקס הם עכשיו הדיבים part1, part2 ו part3, ולא הלינקים המקוריים.

הערך contents למאפיין display, שנתמך ברוב הדפדפנים (חוץ מ IE כמובן), מספק דרך קלה "לדלג" על אלמנטים כשמסדרים אותם בתוך פלקסבוקס או גריד. ה CSS הבא יגרום גם ל HTML השני להציג את הלינקים בצורה יפה בתוך פלקסבוקס:

.topbar {
  display: flex;
}

.topbar > div {
  display: contents;
}

.topbar a {
  padding: 0.2rem;
  flex: 1;
}

וככה זה נראה לייב בקודפן:

איך לקבל את הגדרות הטיפוסים של כל קומפוננטת ריאקט ב TypeScript

בעבודה עם TypeScript כפי שקל להבין לפי שם השפה, אנחנו צריכים להגדיר טיפוס לכל משתנה. עם רוב המשתנים זה לא בעיה אבל מדי פעם אתם רוצים להגיד משהו פרוע כמו "אני מוכן לקבל כל מה ש input מוכן לקבל". לדוגמה בקומפוננטה הזו שמייצרת input יחד עם label:

function InputWithLabel(props: {...}) {
    const { label } = props;
    return (
        <label>{label}</label>
        <input type="text" {...props} />
    );
}

ל TypeScript יש פיתרון נוח למצבים אלה שנקרא React.ComponentProps וזה נראה כך:

type InputProps = React.ComponentProps<'input'>

עכשיו אפשר לחזור לקומפוננטה שלנו ולכתוב:

function InputWithLabel(props: { label: string } & InputProps) {
  const { label } = props;
  return (
    <>
      <label>{label}</label>
      <input type="text" {...props} />
    </>
  );
}

הפונקציה reduce וסימן חלוקה ב 19

אחד הפוסטים שעלה היום בהאקרניוז סיפר על סימן חלוקה חמוד ב 19:

  1. מסתכלים על מספר מימין לשמאל (סיפרה אחרי סיפרה)

  2. כופלים את הסיפרה הימנית ביותר ב 2 ומחברים לה את הסיפרה הבאה

  3. את התוצאה כופלים ב 2 ומחברים לה את הסיפרה הבאה

  4. וכך ממשיכים עד שהגענו למספר קטן יותר, שאולי לגביו נדע אם הוא מתחלק ב-19.

בקוד זה נראה כך:

function reduceMod19(n) {
  let accumulator = 0;
  while (n > 0) {
    let digit = n % 10;
    accumulator = accumulator * 2 + digit;
    n = Math.floor(n / 10);
  }

  return accumulator;
}

הקוד עושה בדיוק מה שהוא צריך לעשות אבל מבחינת קריאות אני חושב שאפשר לשפר: כשאני מסתכל על הלולאה במבט ראשון לא ברור שזו לולאה שרצה על כל הספרות של המספר מימין לשמאל. זה משהו שאני צריך לקרוא ממש את הקוד בשביל להבין אותו.

המטרה של פקודת reduce היא להוציא החוצה את הסיפור הזה. לבוא ולהגיד לקורא - שמע יש פה משהו שרץ על כל האלמנטים של הרשימה אחד אחד. יותר מזה, הדבר הזה יחזיר לך תוצאה בודדת שתהיה מושפעת קצת מכל אלמנט. במילים אחרות reduce לוקחת את כל הקוד והופכת אותו למשפט:

digits.reduce((acc, val) => acc * 2 + val, 0);

אני הולך לעבור על כל הרשימה שנקראת digits אחד-אחד, ועבור כל אלמנט אני אכפול את מה שחישבתי עד עכשיו ב-2 ואוסיף את הדבר החדש.

כמובן בשביל שזה יעבוד אני צריך להפוך את המספר לרשימה של ספרות מהסוף להתחלה וסך הכל שיכתוב הפונקציה נראה כך:

function reduceMod19(n) {
  let digits = String(n).split('').map(d => Number(d));
  return digits.reverse().reduce((acc, val) => acc * 2 + val, 0);
}

התוצאה היא קוד שמספר טוב יותר את הסיפור שלו: במקום להציג לולאה כללית בה אני צריך להתאמץ כדי להבין מה המבנה, אני מקבל כאן מבנה מאוד ספציפי וצריך להתאמץ רק בשביל להבין מה עושים עם כל אלמנט.

כמובן שמשיקולי ביצועים קשה להאמין שהייתי כותב קוד כזה בעולם האמיתי בהקשר הזה של סימן חלוקה ב 19, ובכל זאת שווה להתרגל לשתי הגישות ולהתיחס ל reduce בתור עוד סוג של לולאה - לולאה שמספרת סיפור יותר משמעותי מעוד לולאת for.

איך להפוך את כתיבת ה CSS לקצת פחות מתסכלת

מתכנתים לא אוהבים לכתוב CSS ויש לזה כל מיני סיבות. אפילו אנשי Front End רבים שפגשתי העדיפו להתמקד בצד הטכני יותר של פיתוח JavaScript או React ולהשאיר את ה CSS למעצבים. במקביל ככל שהעולם של ה CSS התפתח כך זה רק עוד יותר הפחיד רבים מאיתנו: עכשיו ללמוד Flexbox ו grid? ובאיזה דפדפנים כל דבר עובד? וממילא מה שאני לא אעשה זה לא יראה טוב אז בשביל מה להתאמץ.

צריך להגיד לכתוב CSS כשלא יודעים CSS זו באמת חוויה קשה, וגם כשיודעים CSS יש בכתיבה שלו אתגרים שונים מאלה של כתיבת קוד. הנה כמה טיפים שעוזרים לי להיות פחות מתוסכל כשאני כותב CSS ואני מקווה שיעזרו גם לכם:

המשך קריאה

הפונקציה getByRole ב react-testing-library

אחד הדברים שהכי קשה להכיל במעבר ל react-testing-library הוא החשיבות הגדולה שמפתחי הספריה מעניקים ל Accessibility Roles. בעוד רוב ספריות הבדיקה נותנות לנו להגיע לאלמנטים באיזו צורה שאנחנו רוצים (דרך CSS או אפילו XPath), כשמגיעים ל react-testing-library מגלים ש getByCSS זו לא פונקציה אמיתית שם.

השינוי הטכני הזה נובע משינוי תפיסה לגבי תפקיד הבדיקה ומה אנחנו רוצים לבדוק: בעוד ספריות בדיקה מסורתיות רוצות לבדוק שדברים מגיעים ל DOM בצורה נכונה, ב react-testing-library אנחנו רוצים לבדוק יותר מזה, אנחנו רוצים לבדוק שאנחנו מבינים את העמוד שלנו כמו שמשתמש מבין אותו. המעבר ל getByRole מהווה שינוי בנקודת המבט.

שימו לב לקטע הבא לדוגמה שהצגתי בוובינר אתמול:

export default function SimpleList({ items }) {
  const [filter, setFilter] = useState('');

  return (
    <>
      <input
        type="search"
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        name="filter-list"
      />
      <ul>
        {items.filter(item => item.text.includes(filter)).map((item, idx) => (
          <li
            key={item.id}
            className={(idx + 1) % 2 === 0 ? "even" : "odd"}
          >
            {item.text}
          </li>
        ))}
      </ul>
    </>
  );
}

בספריית בדיקות רגילה הייתי יכול להשתמש ב CSS כדי להגיע לאלמנט ה input ולמשל לקרוא ל:

document.querySelector('input[name="filter-list"]')

אבל במעבר ל react-testing-library ובפרט אם אני מוכן לקבל את השגעונות שלהם ולהשתמש ב getByRole אני אגלה שקשה לי לתפוס את אלמנט ה input הזה. הקוד הבא יעבוד:

const el = screen.getByRole('searchbox');

נו וזה עובד כל עוד יש תיבה אחת על המסך. הוספה של תיבה נוספת לקומפוננטה תשבור את הבדיקה, ואין לי דרך באמצעות getByRole לציין אף אחד מהמאפיינים של ה input כדי שיעזרו לי לבחור את האלמנט שאני רוצה.

המעבר ל getByRole מכריח אותי לחשוב כמו המשתמש - איך המשתמש יודע על איזו תיבה מדובר? איך המשתמש יודע איפה להכניס את הקלט? משתמש לא רואה את המאפיין name או את הקלאסים של התיבה. הוא יכול לראות את הטקסט שרשום בה אבל בדרך כלל הוא יראה את ה Label שמשויך לתיבת הקלט. וכבר אנחנו רואים את הבעיה בקומפוננטה: ל input אין label שמסביר מה היא עושה.

תיקון של הקוד והוספת label נראה כך:

export default function SimpleList({ items }) {
  const [filter, setFilter] = useState('');

  return (
    <>
      <label>Filter List

        <input
          type="search"
          value={filter}
          onChange={(e) => setFilter(e.target.value)}
        />
      </label>

      <ul>
        {items.filter(item => item.text.includes(filter)).map((item, idx) => (
          <li
            key={item.id}
            className={(idx + 1) % 2 === 0 ? "even" : "odd"}
          >
            {item.text}
          </li>
        ))}
      </ul>
    </>
  );
}

ושימו לב לקסם - עכשיו אפשר להשתמש בטקסט ב label כדי לזהות את ה input שהסתכלנו עליו:

screen.getByRole('searchbox', { name: 'Filter List' } );