# Testing and Debugging

Course: [Astro](https://www.devtutorials.ai/learn/astro) · Section: Ship It
Source: https://www.devtutorials.ai/learn/astro/ship-it/testing-and-debugging

> Reading Astro error messages, server vs browser console.log, the Debug component and the dev toolbar, astro check, unit tests with Vitest and getViteConfig(), component tests with the Container API, and end-to-end tests with Playwright.

## Testing and Debugging

- Every site has bugs. The question is **who finds them first**: you or your visitors.
- **Debugging** means finding out why something is wrong.
- **Testing** means writing small checks that run again and again, so old bugs do not come back.
- In this lesson, you will read errors, print values in the right place, inspect props with `<Debug />`, and test components, functions and whole pages.

**Simple lesson:** debug to find a bug, test to keep it from coming back.

### A Real-World Example

- Asha changes her `formatPrice()` helper. Every price on the shop page breaks.
- She only notices a week later, from an angry email.
- Now she keeps small tests. They run on every change, and a broken helper shows up in seconds.

Press **▶ Run**. This page runs three small tests on a helper and shows the results, like a test runner would.

<ul>
    {tests.map((t) => (
      <li>{t.got === t.want ? "✅" : "❌"} {t.name}: got {t.got}, want {t.want}</li>
    ))}
</ul>
<p id="summary">{passed} of {tests.length} passed</p>
`,
  }}
/>

What happened:

- Each test calls the function with a known input and compares the result with the expected output.
- When every test passes, you can change the code with confidence.
- If a later change breaks the helper, a ❌ appears at once.

### What You Will Learn

In this lesson, you will learn:

1. How to read Astro error messages.
2. Where `console.log()` output goes: terminal or browser.
3. How to inspect values with `<Debug />` and the dev toolbar.
4. How to type-check your project with `astro check`.
5. How to unit-test functions with Vitest.
6. How to test components with the Container API.
7. How to test whole pages in a real browser with Playwright.

## Debugging

### Reading Error Messages

An Astro error tells you three things:

1. **What** went wrong: the message, for example `Cannot read properties of undefined (reading 'join')`.
2. **Where**: the file and line, for example `src/pages/index.astro:4`.
3. Often a **hint**: what to try.

- Read the **first** line of the error carefully. It is usually the real cause.
- `undefined` in a message means a value you expected was missing.

Press **▶ Run**. The page crashes. Read the error, then find the missing `tags` in the data.

<ul>
    {posts.map((p) => <li>{p.title} ({p.tags.join(", ")})</li>)}
</ul>
`,
  }}
/>

- The second post has no `tags`, so `p.tags` is `undefined`.
- Fix: `(p.tags ?? []).join(", ")`. The `??` gives an empty list when tags are missing.

### Where Does console.log() Print?

<figure className="ast-fig ngc-fig">
<svg viewBox="0 0 640 200" role="img" aria-labelledby="fig-log" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-log">Animation: a console.log in the component frontmatter travels to the terminal on the server. A console.log in a script tag travels to the browser's developer tools console.</title>
  <rect x="230" y="60" width="180" height="90" rx="10" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-slate)" }} />
  <text x="320" y="84" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>index.astro</text>
  <text x="320" y="108" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--mono)", fontSize: 11 }}>--- console.log ---</text>
  <text x="320" y="132" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--mono)", fontSize: 11 }}>&lt;script&gt; console.log</text>
  <rect x="20" y="70" width="150" height="70" rx="10" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} />
  <text x="95" y="102" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>Terminal</text>
  <text x="95" y="120" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 10 }}>server / build</text>
  <rect x="470" y="70" width="150" height="70" rx="10" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }} />
  <text x="545" y="102" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>Browser console</text>
  <text x="545" y="120" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 10 }}>F12 → Console</text>
  <circle r="6" cy="104" style={{ fill: "var(--ngc-blue)" }}><animate attributeName="cx" values="230;172" dur="2s" repeatCount="indefinite" /></circle>
  <circle r="6" cy="128" style={{ fill: "var(--ngc-orange)" }}><animate attributeName="cx" values="410;468" dur="2s" repeatCount="indefinite" /></circle>
</svg>
<figcaption>Frontmatter logs appear in the terminal. Script logs appear in the browser.</figcaption>
</figure>

- Code in the **frontmatter** runs on the server (or at build time). Its logs go to the **terminal** where `npm run dev` runs.
- Code in a `<script>` runs in the **browser**. Its logs go to the browser's **DevTools console** (press F12).
- Looking in the wrong place is the most common reason people think "my log does not work".

Press **▶ Run**. In this lab both kinds of log appear in the **Terminal** panel. Notice which one comes first.

<ul>{posts.map((p) => <li>{p}</li>)}</ul>
<button id="b">Click me</button>

<script>
    console.log("script: the page is in the browser now");
    document.getElementById("b")?.addEventListener("click", () => {
      console.log("script: button clicked");
    });
</script>
`,
  }}
/>

### Inspecting Values with Debug

- `<Debug />` from `astro:components` prints any value on the page, nicely formatted.
- Use it to see what a component really received in its props.
- Remove it before you publish.

Press **▶ Run**. The box shows the exact data the card got.

<article>
    <h2>{post.title}</h2>
    <p>{post.likes ?? 0} likes</p>
</article>
`,
    "src/pages/index.astro": `---
---

<h1>Debugging props</h1>
`,
  }}
/>

- **[JS]** Leave out `interface Props`.
- The box shows there is no `likes` field. That is why the card says "0 likes".

### The Dev Toolbar

- In `npm run dev`, Astro adds a small **toolbar** at the bottom of the page.
- **Inspect** highlights islands and shows their props and directives.
- **Audit** finds common accessibility and performance problems, such as images without `alt`.
- **Settings** lets you hide it. It never appears on the built site.

When a page looks wrong, check in this order: the **error overlay**, the **terminal**, the **browser console**, then `<Debug />` on the values you are unsure about.

**Simple lesson:** read the first error line, look for logs in the right place, and print unsure values with `<Debug />`.

## Type Checking with astro check

- TypeScript can find many bugs **before** the page runs, such as a misspelt prop.
- Your editor shows these as red lines, but only for open files.
- `astro check` checks **every** `.astro`, `.ts` and `.tsx` file at once.

```bash
npx astro check
```

- The first time, Astro asks to install `@astrojs/check` and `typescript`. Say yes.
- Add it to the `build` script so a type error stops a broken build:

```json
{
  "scripts": {
    "build": "astro check && astro build"
  }
}
```

`astro check` only reports problems. It never changes your files.

**Simple lesson:** `astro check` type-checks the whole project. Run it before every build.

## Unit Tests with Vitest

### Setting Up Vitest

- **Vitest** is a fast test runner made for Vite projects, and Astro is built on Vite.
- Install it: `npm install -D vitest`.
- Create `vitest.config.ts` with Astro's helper, so tests understand your Astro setup:

```ts
// vitest.config.ts
import { getViteConfig } from "astro/config";

export default getViteConfig({
  test: {
    // Vitest options go here.
  },
});
```

### Writing a Test

```ts
// src/utils/price.test.ts
import { expect, test } from "vitest";
import { formatPrice } from "./price";

test("formats thousands with commas", () => {
  expect(formatPrice(1500)).toBe("₹1,500");
});
```

- `test()` names one check. `expect(value).toBe(wanted)` compares.
- Run all tests with `npx vitest`. It reruns them every time you save a file.

**Simple lesson:** Vitest runs small checks on your functions every time you save.

## Testing Components with the Container API

- The **Container API** renders an Astro component to an HTML string, outside a real site.
- In a test, you render a component with some props and check the HTML.

```ts
// src/components/PostCard.test.ts
import { experimental_AstroContainer as AstroContainer } from "astro/container";
import { expect, test } from "vitest";
import PostCard from "./PostCard.astro";

test("PostCard shows the title", async () => {
  const container = await AstroContainer.create();
  const html = await container.renderToString(PostCard, {
    props: { title: "Islands explained" },
  });
  expect(html).toContain("<h2>Islands explained</h2>");
});
```

- The name starts with `experimental_`: the API may still change a little between versions.

Press **▶ Run**. The lab runs the same kind of component tests with the real Container API and shows the results.

    <h2>{title}</h2>
    <p>{likes} likes</p>
</article>
`,
    "src/pages/index.astro": `---

// In a real project this is PostCard.test.ts, run by Vitest.
const container = await AstroContainer.create();
const withLikes = await container.renderToString(PostCard, { props: { title: "Islands", likes: 5 } });
const noLikes = await container.renderToString(PostCard, { props: { title: "Routing" } });

const tests = [
    { name: "shows the title", ok: withLikes.includes("<h2>Islands</h2>") },
    { name: "shows the likes", ok: withLikes.includes("5 likes") },
    { name: "likes default to 0", ok: noLikes.includes("0 likes") },
];
---

<h1>PostCard tests</h1>
<ul>{tests.map((t) => <li>{t.ok ? "✅" : "❌"} {t.name}</li>)}</ul>
<p id="summary">{tests.filter((t) => t.ok).length} of {tests.length} passed</p>
`,
  }}
/>

- **[JS]** Leave out `interface Props`.

Test what the visitor sees (a title, a price, a link), not every space and attribute. Tests that compare the whole HTML break on harmless changes.

**Simple lesson:** the Container API renders a component in a test, so you can check its HTML.

## End-to-End Tests with Playwright

- An **end-to-end (E2E) test** opens your real site in a real browser and clicks around like a visitor.
- **Playwright** is a popular tool for this. Set it up with `npm init playwright@latest`.

```ts
// tests/home.spec.ts
import { expect, test } from "@playwright/test";

test("the like button counts", async ({ page }) => {
  await page.goto("http://localhost:4321/");
  await page.getByRole("button", { name: /Likes/ }).click();
  await expect(page.getByRole("button", { name: /Likes/ })).toHaveText("👍 Likes: 1");
});
```

- Run with `npx playwright test`. Playwright can start your dev or preview server for you (`webServer` in its config).
- E2E tests are slower than unit tests, so keep them for the most important journeys: home page, login, checkout.

<div className="tscroll">

| Kind of test | Tool | Checks | Speed |
|---|---|---|---|
| Type check | `astro check` | Types in every file | Fast |
| Unit test | Vitest | One function | Very fast |
| Component test | Vitest + Container API | One component's HTML | Fast |
| End-to-end test | Playwright | Whole pages in a browser | Slow |

</div>

**Simple lesson:** Playwright clicks through your real site in a real browser. Use it for the key journeys.

## TL;DR

1. **Errors** = read the first line: what, where (file:line), and the hint.
2. **Logs** = frontmatter → terminal. `<script>` → browser console (F12).
3. **Debug** = `<Debug value={x} />` from `astro:components` prints values on the page.
4. **Dev toolbar** = Inspect islands, Audit for accessibility, only in `npm run dev`.
5. **astro check** = type-check every file. Put it in front of `astro build`.
6. **Vitest** = unit tests with `getViteConfig()`. `test()`, `expect().toBe()`.
7. **Container API** = `renderToString(Component, { props })` in a test.
8. **Playwright** = end-to-end tests in a real browser for key journeys.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Looking for frontmatter logs in the browser | All logs go to DevTools | Frontmatter logs go to the terminal | "My log does not work" |
| Reading only the last error line | The end is the cause | The first line is usually the cause | Fixing the wrong thing |
| Leaving `<Debug />` in the page | It is hidden in production | It shows to everyone | Data leaks on the live site |
| Skipping `astro check` | The editor catches everything | It only checks open files | Type errors reach the build |
| Testing the whole HTML string | Exact HTML is the best test | Test what the visitor sees | Tests break on harmless changes |
| Only E2E tests | One kind is enough | Mix fast unit tests and a few E2E tests | Slow, flaky test runs |

</div>

## Check Yourself

## Exercises

## What to Remember

- Read the first line of an error: what, where, and the hint.
- Frontmatter logs go to the terminal, script logs to the browser console.
- Use `<Debug />` and the dev toolbar to see values and islands.
- Run `astro check` before builds, Vitest for functions and components, Playwright for key journeys.

Next: **Deploying Your Site**, where Asha's journal goes live on the internet.
