Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: container API docs #8281

Merged
merged 12 commits into from
May 22, 2024
25 changes: 25 additions & 0 deletions src/content/docs/en/guides/testing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ i18nReady: true
---
import { Steps } from '@astrojs/starlight/components';
import PackageManagerTabs from '~/components/tabs/PackageManagerTabs.astro';
import Since from '~/components/Since.astro'

Testing helps you write and maintain working Astro code. Astro supports many popular tools for unit tests, component tests, and end-to-end tests including Jest, Mocha, Jasmine, [Cypress](https://cypress.io) and [Playwright](https://playwright.dev). You can even install framework-specific testing libraries such as React Testing Library to test your UI framework components.

Expand Down Expand Up @@ -43,6 +44,30 @@ export default getViteConfig(

See the [Astro + Vitest starter template](https://github.com/withastro/astro/tree/latest/examples/with-vitest) on GitHub.

## Vitest and Container API

<Since v="4.9.0" />

You can natively test Astro components using the new [container API](/en/reference/container-reference/). First, setup [`vitest` as explained above](#vitest), then create a `.test.js` file to test your component:
ematipico marked this conversation as resolved.
Show resolved Hide resolved

```js
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { expect, test } from 'vitest';
import Card from '../src/components/Card.astro';

test('Card with slots', async () => {
const container = await AstroContainer.create();
const result = await container.renderToString(Card, {
slots: {
default: 'Card content',
},
});

expect(result).toContain('This is a card');
expect(result).toContain('Card content');
});
```

## Cypress

Cypress is a front-end testing tool built for the modern web. Cypress enables you to write end-to-end tests for your Astro site.
Expand Down
292 changes: 292 additions & 0 deletions src/content/docs/en/reference/container-reference.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
---
title: Astro Container API (experimental)
i18nReady: false
---
import Since from '~/components/Since.astro'

The Container API allows to render Astro components in isolation.

<Since v="4.9.0" />
ematipico marked this conversation as resolved.
Show resolved Hide resolved

## `create()`

Creates a new instance of the container.

```js
import { experimental_AstroContainer } from "astro/container";

const container = await experimental_AstroContainer.create();
```

It accepts an object with the following options
ematipico marked this conversation as resolved.
Show resolved Hide resolved


```ts
export type AstroContainerOptions = {
streaming?: boolean;
renderers?: AstroRenderer[];
astroConfig?: AstroContainerUserConfig;
};
```

#### `streaming` option

Enables rendering components using the streaming.

**Type:** `boolean`
ematipico marked this conversation as resolved.
Show resolved Hide resolved

#### `renderers` option

A list of client renderers. Add a renderer if your Astro component renders some client components such as React, Preact, etc.

**Type:**: `AstroRenderer[]`;

For example, if you're rendering a Astro component that renders a React component, you'll have to provide the following renderer:
ematipico marked this conversation as resolved.
Show resolved Hide resolved

```js
const container = await experimental_AstroContainer.create({
renderers: [
{
name: "@astrojs/react",
client: "@astrojs/react/client.js",
server: "@astrojs/react/server.js"
}
]
})
const result = await container.renderToString(ReactWrapper);
```

#### `astroConfig` option

A subset of the Astro user configuration.

**Type**: `AstroContainerUserConfig`
ematipico marked this conversation as resolved.
Show resolved Hide resolved

## `renderToString()`

It renders a component, and it returns a string that represents the HTML/content rendered by the Astro component.
ematipico marked this conversation as resolved.
Show resolved Hide resolved

```js
import { experimental_AstroContainer } from "astro/container";
import Card from "../src/components/Card.astro";

const container = await experimental_AstroContainer.create();
const result = await container.renderToString(Card);
```

Under the hood, this function calls [`renderToResponse`](#rendertoresponse) and calls `Response.text()`.

It accepts a [number of options](#rendering-options).
ematipico marked this conversation as resolved.
Show resolved Hide resolved

## `renderToResponse()`

It renders a component, and it returns a `Response` object.

```js
import { experimental_AstroContainer } from "astro/container";
import Card from "../src/components/Card.astro";

const container = await experimental_AstroContainer.create();
const result = await container.renderToResponse(Card);
```

It accepts a [number of options](#rendering-options).
ematipico marked this conversation as resolved.
Show resolved Hide resolved

### Rendering options

Both [`renderToResponse`](#rendertoresponse) and [`renderToString`](#rendertostring) accept an object as their second argument:

```ts
export type ContainerRenderOptions = {
slots?: Record<string, any>;
request?: Request;
params?: Record<string, string | undefined>;
locals?: App.Locals;
routeType?: "page" | "endpoint";

};
```

ematipico marked this conversation as resolved.
Show resolved Hide resolved
#### `slots`

**Type**: `Record<string, any>`;

Use this options if your component needs to render some [slots](en/basics/astro-components/#slots).

If your components renders one slot, pass an object with `default` as key:
ematipico marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If your components renders one slot, pass an object with `default` as key:
If your Astro component renders one slot, pass an object with `default` as the key:


```js name="Card.test.js"
import Card from "../src/components/Card.astro";

const result = await container.renderToString(Card, {
slots: { default: "Some value"}
});
```

If your component renders named slots, use the keys of the object to name the slots:
ematipico marked this conversation as resolved.
Show resolved Hide resolved

```astro name="Card.astro"
---
---
<div>
<slot name="header" />
<slot name="footer" />
</div>
```

```js name="Card.test.js"
import Card from "../src/components/Card.astro";

const result = await container.renderToString(Card, {
slots: { "header": "Header content", "footer": "Footer" }
});
```

You can also render components in cascade:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we'd say "in cascade" but I get what you mean. Bookmarking this!


```astro name="Card.astro"
---
---
<div>
<slot name="header" />
<slot name="footer" />
</div>
```

```js name="Card.test.js"
import Card from "../src/components/Card.astro";
import CardHeader from "../src/components/CardHeader.astro";
import CardFooter from "../src/components/CardFooter.astro";

const result = await container.renderToString(Card, {
slots: {
"header": await container.renderToString(CardHeader),
"footer": await container.renderToString(CardFooter),
}
});
```

#### `request` option

**Type**: `Request`

The request is used to understand which path/URL the component is about to render.

Use this option in case your component needs to read information like `Astro.url` or `Astro.request`.
ematipico marked this conversation as resolved.
Show resolved Hide resolved

You can also inject possible headers or cookies.

```js file="Card.test.js"
import Card from "../src/components/Card.astro";

const result = await container.renderToString(Card, {
request: new Request("https://example.com/blog", {
headers: {
"X-some-secret-header": "test-value"
}
})
});
```

#### `params` option

**Type**: `Record<string, string | undefined>`;

Use this option in case your component needs to read `Astro.params`. As opposed to `getStaticPaths`, you must provide only **one** item of the params returned by that function.
ematipico marked this conversation as resolved.
Show resolved Hide resolved

```astro name="Card.astro"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checking, will Astro.params usually be used by a file generating dynamic routes?

If so, does it make sense for maybe this title name to be of the style src/pages/[slug].astro ? And then, the test example would need to be updated. Can you double check this whole example to make sure it reflects a very common use case?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think it makes more sense to use a file with a dynamic route

---
const { locale, slug } = Astro.params;
---
<div></div>
```

```js file="Card.test.js"
import Card from "../src/components/Card.astro";

const result = await container.renderToString(Card, {
params: {
locale: "en",
slug: "getting-started"
}
});
```

#### `locals` options

**Type**: `App.Locals`

Use this option to stub the `Astro.locals` object:
ematipico marked this conversation as resolved.
Show resolved Hide resolved

```astro name="Card.astro"
---
const { checkAuth } = Astro.locals;
const isAuthenticated = checkAuth();
---
{isAuthenticated ? <span>You're in</span> : <span>You're out</span> }
```

```js file="Card.test.js"
import Card from "../src/components/Card.astro";

test("User is in", async () => {
const result = await container.renderToString(Card, {
locals: {
checkAuth() { return true }
}
});

// assert result contains "You're in"
})


test("User is out", async () => {
const result = await container.renderToString(Card, {
locals: {
checkAuth() { return false }
}
});

// assert result contains "You're out"
})
```

#### `routeType` option

**Type**: `"page" | "endpoint"`

Useful in case you're rendering an endpoint. In this case, you want to use `renderToResponse`:
ematipico marked this conversation as resolved.
Show resolved Hide resolved

```js
container.renderToString(Endpoint, { routeType: "endpoint" });
```

```js file="endpoint.test.js"
import * as Endpoint from "../src/pages/api/endpoint.js";

const response = await container.renderToResponse(Endpoint, {
routeType: "endpoint"
});
const json = await response.json();
```

If your endpoint needs to be tested on methods such as `POST`, `PATCH`, etc., you'll have to use the `request` option to signal Astro to call the correct function:
ematipico marked this conversation as resolved.
Show resolved Hide resolved

```js file="endpoint.js"
export function GET() {}

// need to test this
export function POST() {}
```

```js file="endpoint.test.js" ins={5-7}
import * as Endpoint from "../src/pages/api/endpoint.js";

const response = await container.renderToResponse(Endpoint, {
routeType: "endpoint",
request: new Request("https://example.com", {
method: "POST" //
})
});
const json = await response.json();
```
5 changes: 5 additions & 0 deletions src/i18n/en/nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ export default [
slug: 'reference/dev-toolbar-app-reference',
key: 'reference/dev-toolbar-app-reference',
},
{
text: 'Container API',
slug: 'reference/container-reference',
key: 'reference/container-reference',
},
{
text: 'Template Directives',
slug: 'reference/directives-reference',
Expand Down
Loading