Skip to content

Commit

Permalink
feat: container API docs (#8281)
Browse files Browse the repository at this point in the history
Co-authored-by: Sarah Rainsberger <[email protected]>
  • Loading branch information
ematipico and sarah11918 authored May 22, 2024
1 parent 70e6299 commit 8a09935
Show file tree
Hide file tree
Showing 3 changed files with 325 additions and 0 deletions.
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 @@ -45,6 +46,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 [container API](/en/reference/container-reference/). First, setup [`vitest` as explained above](#vitest), then create a `.test.js` file to test your component:

```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');
});
```

## End-to-end tests

### Playwright
Expand Down
295 changes: 295 additions & 0 deletions src/content/docs/en/reference/container-reference.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
---
title: Astro Container API (experimental)
i18nReady: false
---
import Since from '~/components/Since.astro'

<p><Since v="4.9.0" /></p>

The Container API allows you to render Astro components in isolation.

This experimental server-side API unlocks a variety of potential future uses, but is currently scoped to allow [unit testing of `.astro` component output](/en/guides/testing/#vitest-and-container-api). This API allows you to create a new container, and render an Astro component returning a string or a `Response`.

This API is experimental and subject to breaking changes, even in [minor or patch releases](/en/upgrade-astro/#semantic-versioning). Please consult [the Astro CHANGELOG](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md) for changes as they occur. This page will always be updated with the most current information for the latest version of Astro.

## `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:


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

#### `streaming` option

**Type:** `boolean`

Enables rendering components using [HTML streaming](/en/guides/server-side-rendering/#html-streaming).

#### `renderers` option

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

A list of client renderers required by the component. Use this if your `.astro` component renders any [UI framework components](/en/guides/framework-components/) using an official Astro integration (e.g. React, Vue, etc.). For each framework rendered, you must provide an object stating the integration package `name`, as well as both its `client` and `server` rendering script

The following example provides the necessary object to render an Astro component that renders a React component:

```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);
```

## `renderToString()`

This function renders a specified component inside a container. It takes an Astro component as an argument and it returns a string that represents the HTML/content rendered by the Astro component.

```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 also accepts an object as a second argument that can contain a [number of options](#rendering-options).

## `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 also accepts an object as a second argument that can contain a [number of options](#rendering-options).

### 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";

};
```

These optional values can be passed to the rendering function in order to provide additional information necessary for an Astro component to properly render.

#### `slots`

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

An option to pass content to be rendered with [`<slots>`](en/basics/astro-components/#slots).

If your Astro component renders one default 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 slot names as the object keys:

```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:

```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`

An option to pass a `Request` with information about the path/URL the component will render.

Use this option when your component needs to read information like `Astro.url` or `Astro.request`.

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>`;

An object to pass information about the path parameter to an Astro component responsible for [generating dynamic routes](/en/guides/routing/#dynamic-routes).

Use this option when your component needs a value for `Astro.params` in order to generate a single route dynamically.

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

```js file="LocaleSlug.test.js"
import LocaleSlug from "../src/components/[locale]/[slug].astro";

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

#### `locals` options

**Type**: `App.Locals`

An option to pass information from [`Astro.locals`](/en/reference/api-reference/#astrolocals) for rendering your component.

Use this option to when your component needs information stored during the lifecycle of a request in order to render, such as logged in status.

```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"`

An option available when using `renderToResponse` to specify that you are rendering an [endpoint](/en/guides/endpoints/):

```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();
```

To test your endpoint on methods such as `POST`, `PATCH`, etc., use the `request` option to call the correct function:

```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

0 comments on commit 8a09935

Please sign in to comment.