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

};
```

ematipico marked this conversation as resolved.
Show resolved Hide resolved
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:
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`

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"
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="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
Loading