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: New test package #158

Merged
merged 18 commits into from
Sep 10, 2024
Merged
Prev Previous commit
Next Next commit
Document all APIs
Fryuni committed Sep 5, 2024
commit 134b3d2a3e8a4bc71ac07b2847bb03d66b020aa5
162 changes: 150 additions & 12 deletions docs/src/content/docs/astro-tests.mdx
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ import { PackageManagers } from 'starlight-package-managers';

<p>**Type:** `AstroConfig`</p>

The final config passed to [Astro's programatic CLI entrypoints][advanced APIs]. This configuration can be overriden on each method call.
The final config passed to [Astro's programatic CLI entrypoints](https://docs.astro.build/en/reference/cli-reference/#advanced-apis-experimental). This configuration can be overriden on each method call.
Fryuni marked this conversation as resolved.
Show resolved Hide resolved
Will be automatically passed to the methods below:
Fryuni marked this conversation as resolved.
Show resolved Hide resolved

- [`startDevServer()`](#startdevserver)
@@ -138,76 +138,214 @@ Equivalent to running [`astro sync`](https://docs.astro.build/en/reference/cli-r
**Type:** `() => Promise<void>`
</p>

Deletes the generated files from the fixture directory. Specifically, it deletes:

- The output directory (`outDir` config)
- The cache directory (`cacheDir` config)
- The `.astro` directory generated in the project
- the `.astro` directory generated in the `node_modules`

### `resolveUrl`

<p>**Type:** `(url: string) => string`</p>

Resolves a relative URL to the full url of the running server.

This can only be called after either [`.startDevServer()`](#startdevserver) or [`.preview()`](#preview) is called.

### `fetch`

{(

<p>
**Type:** `(url: string, opts?: Parameters<typeof request>[1]) => Promise<Response>`
<strong>Type: </strong>
<code dir="auto" style="padding-right: 0;">
(url: string, opts?:{' '}
</code>
<a href="https://developer.mozilla.org/en-US/docs/Web/API/RequestInit">
<code dir="auto" style="padding-left: 0;padding-right: 0;">
RequestInit
</code>
</a>
<code dir="auto" style="padding-left: 0;padding-right: 0;">
) =&gt; Promise&lt;
</code>
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Response">
<code dir="auto" style="padding-left: 0;padding-right: 0;">
Response
</code>
</a>
<code dir="auto" style="padding-left: 0;">
&gt;
</code>
</p>
)}

Send a request to the given URL. If the URL is relative, it will be resolved relative to the root of the server (without a base path).

This can only be called after either [`.startDevServer()`](#startdevserver) or [`.preview()`](#preview) is called.

### `pathExists`

<p>**Type:** `(path: string) => boolean`</p>

Checks whether the given path exists on the build output (`outDir` config).

### `readFile`

<p>
**Type:** `(path: string) => Promise<string>`
</p>

Read a file from the build (relative to `outDir` config).

### `editFile`

<p>
**Type:** `(path: string, updater: string | ((content: string) => string)) => Promise<() => void>`
</p>

Edit a file in the fixture directory.

The first parameter is a path relative to the root of the fixture.

The second parameter can be the new content of the file or a function that takes the current content and returns the new content.

This function returns a Promise that resolves to another function. This resolved function can be called to revert the changes.

All changes made with `editFile` are automatically reverted before the [process exits](https://nodejs.org/api/process.html#event-exit).

### `resetAllFiles`

<p>**Type:** `() => void`</p>

Reset all changes made with [`.editFile()`](#editfile)

### `readdir`

<p>
**Type:** `(path: string) => Promise<string[]>`
</p>

Read a directory from the build output (relative to `outDir` config).

This is a convenience wrapper around [readdir from Node's FS Promise API](https://nodejs.org/api/fs.html#fspromisesreaddirpath-options).
Fryuni marked this conversation as resolved.
Show resolved Hide resolved

### `glob`

<p>
**Type:** `(pattern: string) => Promise<string[]>`
</p>

### `loadTestAdapterApp`
Find entries in the build output matching the glob pattern.

The glob syntax used is from [`fast-glob`](https://www.npmjs.com/package/fast-glob#pattern-syntax).

### `loadNodeAdapterHandler`

{(

<p>
**Type:** `() => Promise<App>`
<strong>Type: </strong>
<code dir="auto" style="padding-right: 0;">
() =&gt; Promise&lt;(req:{' '}
</code>
<a href="https://nodejs.org/api/http.html#class-httpincomingmessage">
<code dir="auto" style="padding-left: 0;padding-right: 0;">
http.IncomingMessage
</code>
</a>
<code dir="auto" style="padding-left: 0;padding-right: 0;">
, res:{' '}
</code>
<a href="https://nodejs.org/api/http.html#class-httpserverresponse">
<code dir="auto" style="padding-left: 0;padding-right: 0;">
http.ServerResponse
</code>
</a>
<code dir="auto" style="padding-left: 0;">
) =&gt; void&gt;
</code>
</p>
)}

### `loadNodeAdapterHandler`
Load the handler for an app built using the [Node Adapter](https://docs.astro.build/en/guides/integrations-guide/node/).

The handler is the same as a listener for the [`request` event](https://nodejs.org/api/http.html#event-request) from Node's native HTTP module.

### `loadTestAdapterApp`

<p>
**Type:** `() => Promise<(req: NodeRequest, res: NodeResponse) => void>`
**Type:** `() => Promise<TestApp>`
</p>

#### `TestApp`

```ts
type TestApp = {
render: (req: Request) => Promise<Response>;
toInternalApp: () => App;
};
```

A minimal proxy to the underlying Astro App using the test adapter.
Fryuni marked this conversation as resolved.
Show resolved Hide resolved

##### `render`

Renders a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) from the given [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request).

##### `toInternalApp`

Returns the underlying [Astro App](https://github.com/withastro/astro/blob/ca54e3f819fad009ac3c3c8b57a26014a2652a73/packages/astro/src/core/app/index.ts#L77-L518).

:::danger
This class is internal, undocumented and highly unstable. Use it at your own risk.
:::

## Test Adapter

### `env`
An [Astro Adapter](https://docs.astro.build/en/guides/server-side-rendering/) that exposes the rendering process to be called directly.

### `provideAddress`
It also collects information about the build passed to the adapter to be inspected.

### `extendAdapter`
None of the options are required.

### `setEntryPoints`
### `env`

### `setMiddlewareEntryPoint`
<p>
**Type:** `Record<string, string | undefined>`
</p>

Server-side environment variables to be used by [`astro:env`](https://docs.astro.build/en/reference/configuration-reference/#experimentalenv).

### `setRoutes`

{(

<p>
<strong>Type: </strong>
<code dir="auto" style="padding-right: 0;">
(routes:{' '}
</code>
<a href="https://docs.astro.build/en/reference/integrations-reference/#routedata-type-reference">
<code dir="auto" style="padding-left: 0;padding-right: 0;">
RouteData
</code>
</a>
<code dir="auto" style="padding-left: 0;">
[]) =&gt; Promise&lt;void&gt;
</code>
</p>
)}

A callback function that will receive the final value of the project routes.

## Utilities

### No Node checker

[advanted APIs]: https://docs.astro.build/en/reference/cli-reference/#advanced-apis-experimental
A Vite plugin to ensure no module in the final bundle relies on the built-in Node modules.
Fryuni marked this conversation as resolved.
Show resolved Hide resolved

If the generated bundle contains any reference to a Node module the build will fail.
Fryuni marked this conversation as resolved.
Show resolved Hide resolved

The checked modules are those from the built-in module list provided as [part of `node:modules`](https://nodejs.org/api/module.html#modulebuiltinmodules), both with an without the `node:` prefix, as well as the [prefix-only modules](https://nodejs.org/api/modules.html#built-in-modules-with-mandatory-node-prefix).
58 changes: 42 additions & 16 deletions packages/astro-tests/src/astroFixture.ts
Original file line number Diff line number Diff line change
@@ -7,9 +7,9 @@ import { build, dev, preview, sync } from 'astro';
import type { AstroConfig, AstroInlineConfig } from 'astro';
import { mergeConfig } from '../node_modules/astro/dist/core/config/merge.js';
import { validateConfig } from '../node_modules/astro/dist/core/config/validate.js';
import type { App } from 'astro/app';
import { getViteConfig } from 'astro/config';
import { callsites } from './utils.js';
import type { App } from 'astro/app';

// Disable telemetry when running tests
process.env.ASTRO_TELEMETRY_DISABLED = 'true';
@@ -20,6 +20,11 @@ export type NodeResponse = import('node:http').ServerResponse;
export type DevServer = Awaited<ReturnType<typeof dev>>;
export type PreviewServer = Awaited<ReturnType<typeof preview>>;

type TestApp = {
render: (req: Request) => Promise<Response>;
toInternalApp: () => App;
};

type Fixture = {
/**
* Returns the final config.
@@ -62,17 +67,25 @@ type Fixture = {
sync: typeof sync;

/**
* Removes the project's dist folder.
* Removes generated directories from the fixture directory.
*/
clean: () => Promise<void>;
/**
* Resolves a relative URL to the full url of the running server.
*
* This can only be called after either .startDevServer() or .preview() is called.
*/
resolveUrl: (url: string) => string;
/**
* Returns a URL from the running server.
* Send a request to the given URL. If the URL is relative, it will be resolved relative to the root of the server (without a base path).
*
* Must have called .dev() or .preview() before.
* This can only be called after either .startDevServer() or .preview() is called.
*/
fetch: (url: string, opts?: Parameters<typeof request>[1]) => Promise<Response>;

/**
* Checks whether the given path exists on the build output.
*/
pathExists: (path: string) => boolean;
/**
* Read a file from the build.
@@ -92,7 +105,7 @@ type Fixture = {
*/
resetAllFiles: () => void;
/**
* Read a directory from the build.
* Read a directory from the build output.
*/
readdir: (path: string) => Promise<string[]>;

@@ -103,9 +116,9 @@ type Fixture = {
/**
* Load an app built using the Test Adapter.
*/
loadTestAdapterApp: () => Promise<App>;
loadTestAdapterApp: () => Promise<TestApp>;
/**
* Load an app built using the Node Adapter.
* Load the handler for an app built using the Node Adapter.
*/
loadNodeAdapterHandler: () => Promise<(req: NodeRequest, res: NodeResponse) => void>;
};
@@ -223,14 +236,24 @@ export async function loadFixture(inlineConfig: InlineConfig): Promise<Fixture>
recursive: true,
force: true,
});
const astroCache = new URL('./node_modules/.astro', config.root);
if (fs.existsSync(astroCache)) {
await fs.promises.rm(astroCache, {
maxRetries: 10,
recursive: true,
force: true,
});
}

await fs.promises.rm(new URL('./node_modules/.astro', config.root), {
maxRetries: 10,
recursive: true,
force: true,
});

await fs.promises.rm(config.cacheDir, {
maxRetries: 10,
recursive: true,
force: true,
});

await fs.promises.rm(new URL('./.astro', config.root), {
maxRetries: 10,
recursive: true,
force: true,
});
},
resolveUrl,
fetch: async (url, init) => {
@@ -311,7 +334,10 @@ export async function loadFixture(inlineConfig: InlineConfig): Promise<Fixture>
const { createApp, manifest } = await import(url.toString());
const app = createApp();
app.manifest = manifest;
return app;
return {
render: (req) => app.render(req),
toInternalApp: () => app,
};
},
loadNodeAdapterHandler: async () => {
const url = new URL(`./server/entry.mjs?id=${fixtureId}`, config.outDir);
11 changes: 9 additions & 2 deletions packages/astro-tests/src/noNodeModule.ts
Original file line number Diff line number Diff line change
@@ -6,13 +6,20 @@
import type { Plugin } from 'vite';
import { builtinModules } from 'node:module';

const nodeModules = [
...builtinModules,
...builtinModules.map((modName) => `node:${modName}`),
'node:test',
'node:test/reporters',
'node:sea',
];

export function preventNodeBuiltinDependencyPlugin(): Plugin {
// Verifies that `astro:content` does not have a hard dependency on Node builtins.
// Verifies that the final bundle does not have a hard dependency on Node builtins.
// This is to verify it will run on Cloudflare and Deno
return {
name: 'verify-no-node-stuff',
generateBundle() {
const nodeModules = builtinModules.map((modName) => `node:${modName}`).concat(builtinModules);
nodeModules.forEach((name) => {
const mod = this.getModuleInfo(name);
if (mod) {
Loading