לדלג לתוכן

10.9 Lighthouse תרגול

תרגול - לייטהאוס ואסטרטגיית בדיקות - Lighthouse and Testing Strategy

תרגיל 1 - הגדרת Lighthouse CI

הגדירו Lighthouse CI לפרויקט Next.js שכולל:

  1. קובץ lighthouserc.json עם:
  2. בדיקה של 3 דפים (בית, מוצרים, אודות)
  3. 5 הרצות לכל דף (לממוצע אמין)
  4. ציון מינימלי: Performance 90, Accessibility 95, SEO 90, Best Practices 90
  5. שמירת דוחות

  6. GitHub Action שמריץ את Lighthouse CI ב-PR

  7. סקריפט בדיקה מקומי ב-package.json


תרגיל 2 - ניתוח ותיקון דוח Lighthouse

נתון דוח Lighthouse עם הציונים הבאים:
- Performance: 45
- Accessibility: 62
- Best Practices: 78
- SEO: 55

הבעיות שזוהו:

ביצועים:
- LCP: 6.2 שניות (תמונת hero של 3MB ב-JPEG)
- TBT: 1,200 מ"ש (bundle של 800KB)
- CLS: 0.35 (תמונות ללא dimensions, פונט FOUT)
- Unused JavaScript: 450KB

נגישות:
- 5 תמונות ללא alt
- 3 inputs ללא label
- ניגודיות צבעים נמוכה (2.5:1) על 4 אלמנטים
- אין lang attribute

SEO:
- אין meta description
- אין viewport meta
- קישורים עם "לחצו כאן" כ-anchor text

כתבו תוכנית תיקון מפורטת עם קוד לדוגמה לכל בעיה. סדרו לפי עדיפות.


תרגיל 3 - תכנון אסטרטגיית בדיקות

אתם מפתחים אפליקציית ניהול משימות (Todo App) עם הפיצ'רים הבאים:
- הרשמה והתחברות
- יצירה, עריכה, מחיקה של משימות
- סינון לפי סטטוס (הכל, פעילות, הושלמו)
- גרירה ושחרור לשינוי סדר
- תזכורות (notifications)
- שיתוף משימות עם משתמשים אחרים
- סטטיסטיקות (כמה משימות הושלמו החודש)

תכננו אסטרטגיית בדיקות מלאה:
1. רשימת בדיקות יחידה (לפחות 10)
2. רשימת בדיקות אינטגרציה (לפחות 6)
3. רשימת בדיקות E2E (לפחות 4)
4. הגדרת coverage thresholds
5. הגדרת CI pipeline


תרגיל 4 - כתיבת בדיקות לפרויקט קיים

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

// utils/date.ts
export function formatDate(date: Date): string {
  return new Intl.DateTimeFormat('he-IL', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  }).format(date);
}

export function isOverdue(dueDate: Date): boolean {
  return new Date() > dueDate;
}

export function daysUntil(date: Date): number {
  const now = new Date();
  const diff = date.getTime() - now.getTime();
  return Math.ceil(diff / (1000 * 60 * 60 * 24));
}

export function getRelativeTime(date: Date): string {
  const now = new Date();
  const diffMs = now.getTime() - date.getTime();
  const diffMinutes = Math.floor(diffMs / (1000 * 60));
  const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
  const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));

  if (diffMinutes < 1) return 'הרגע';
  if (diffMinutes < 60) return `לפני ${diffMinutes} דקות`;
  if (diffHours < 24) return `לפני ${diffHours} שעות`;
  if (diffDays < 30) return `לפני ${diffDays} ימים`;
  return formatDate(date);
}
// components/TaskItem.tsx
interface Task {
  id: string;
  title: string;
  completed: boolean;
  dueDate: Date | null;
}

function TaskItem({
  task,
  onToggle,
  onDelete,
  onEdit,
}: {
  task: Task;
  onToggle: (id: string) => void;
  onDelete: (id: string) => void;
  onEdit: (id: string, title: string) => void;
}) {
  const [editing, setEditing] = useState(false);
  const [editTitle, setEditTitle] = useState(task.title);

  function handleSave() {
    if (editTitle.trim()) {
      onEdit(task.id, editTitle);
      setEditing(false);
    }
  }

  const isOverdue = task.dueDate && !task.completed && new Date() > task.dueDate;

  return (
    <li className={isOverdue ? 'overdue' : ''}>
      <input
        type="checkbox"
        checked={task.completed}
        onChange={() => onToggle(task.id)}
        aria-label={`סמן ${task.title} כ${task.completed ? 'לא' : ''} הושלם`}
      />
      {editing ? (
        <input
          value={editTitle}
          onChange={e => setEditTitle(e.target.value)}
          onBlur={handleSave}
          onKeyDown={e => e.key === 'Enter' && handleSave()}
          autoFocus
        />
      ) : (
        <span
          style={{ textDecoration: task.completed ? 'line-through' : 'none' }}
          onDoubleClick={() => setEditing(true)}
        >
          {task.title}
        </span>
      )}
      {task.dueDate && (
        <time dateTime={task.dueDate.toISOString()}>
          {formatDate(task.dueDate)}
        </time>
      )}
      <button onClick={() => onDelete(task.id)} aria-label={`מחק ${task.title}`}>
        מחק
      </button>
    </li>
  );
}

כתבו:
- בדיקות יחידה ל-date.ts (לפחות 12 בדיקות)
- בדיקות אינטגרציה ל-TaskItem (לפחות 8 בדיקות)


תרגיל 5 - CI Pipeline מלא

צרו GitHub Actions workflow מלא שכולל:

  1. שלב lint: ESLint + TypeScript type check
  2. שלב בדיקות יחידה: Vitest עם coverage
  3. שלב build: npm run build
  4. שלב בדיקות E2E: Playwright (רק על Chromium ב-CI)
  5. שלב Lighthouse: בדיקת 2 דפים עם ספים

הדרישות:
- בדיקות יחידה רצות במקביל ל-lint
- E2E רץ רק אחרי build
- Lighthouse רץ רק אחרי build
- כל השלבים מעלים artifacts
- ב-PR: הכל רץ. ב-push ל-main: רק lint + unit + build


תרגיל 6 - דוח בדיקות אוטומטי

כתבו סקריפט שמריץ את כל הבדיקות ומייצר דוח HTML מסכם:

הדוח צריך לכלול:
- תאריך ושעה
- תוצאות בדיקות יחידה (עברו/נכשלו/דילגו)
- אחוזי כיסוי קוד (statements, branches, functions, lines)
- תוצאות בדיקות E2E
- ציוני Lighthouse (אם זמינים)
- רשימת בדיקות שנכשלו עם פרטים

# הסקריפט צריך להיקרא כך:
npm run test:report

שאלות

  1. מה ההבדל בין Lab Data ל-Field Data ב-Lighthouse? למה הציונים עשויים להיות שונים?
  2. למה 100% code coverage אינו יעד מומלץ? מה היעד הנכון?
  3. כיצד מחליטים מה לבדוק בבדיקת E2E ומה בבדיקת יחידה?
  4. מה היתרון של הרצת Lighthouse ב-CI לעומת בדיקה ידנית?
  5. תארו מצב שבו בדיקות עוברות אבל האפליקציה שבורה. כיצד ניתן למנוע מצב כזה?