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: server exports (#130) #179

Merged
merged 10 commits into from
May 26, 2024
43 changes: 37 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ Zimic provides a flexible and type-safe way to mock HTTP requests.
- [`zimic browser init`](#zimic-browser-init)
- [`zimic server`](#zimic-server)
- [`zimic server start`](#zimic-server-start)
- [Programmatic usage](#programmatic-usage)
- [Changelog](#changelog)

## Getting started
Expand Down Expand Up @@ -1807,13 +1808,21 @@ Positionals:
[string]

Options:
-h, --hostname The hostname to start the server on.
--help Show help [boolean]
--version Show version number [boolean]
-h, --hostname The hostname to start the server on.
[string] [default: "localhost"]
-p, --port The port to start the server on. [number]
-e, --ephemeral Whether the server should stop automatically after the
on-ready command finishes. If no on-ready command is provided
and ephemeral is true, the server will stop immediately after
starting. [boolean] [default: false]
-p, --port The port to start the server on. [number]
-e, --ephemeral Whether the server should stop automatically
after the on-ready command finishes. If no
on-ready command is provided and ephemeral is
true, the server will stop immediately after
starting. [boolean] [default: false]
-l, --log-unhandled-requests Whether to log a warning when no interceptors
were found for the base URL of a request. If an
interceptor was matched, the logging behavior
for that base URL is configured in the
interceptor itself. [boolean]
```

You can use this command to start an independent server:
Expand All @@ -1831,6 +1840,28 @@ zimic server start --port 4000 --ephemeral -- npm run test
The command after `--` will be executed when the server is ready. The flag `--ephemeral` indicates that the server
should automatically stop after the command finishes.

#### Programmatic usage

The module `zimic/server` exports resources for managing interceptor servers programmatically. Even though we recommend
using the CLI, this module is a valid alternative for more advanced use cases.

```ts
import { createInterceptorServer, runCommand } from 'zimic/server';

const server = createInterceptorServer({ hostname: 'localhost', port: 3000 });
await server.start();

// Run a command when the server is ready
const [command, ...commandArguments] = process.argv.slice(3);
await runCommand(command, commandArguments);

await server.stop();
```

The helper function `runCommand` is useful to run a shell command in server scripts. The
[Next.js App Router](./examples/README.md#nextjs) and the [Playwright](./examples/README.md#playwright) examples use
this function to run the application after the interceptor server is ready and all mocks are set up.

---

## Changelog
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, expectTypeOf, it } from 'vitest';
import {
createInterceptorServer,
InterceptorServer,
InterceptorServerOptions,
NotStartedInterceptorServerError,
runCommand,
CommandError,
DEFAULT_ACCESS_CONTROL_HEADERS,
DEFAULT_PREFLIGHT_STATUS_CODE,
} from 'zimic0/server';

describe('Exports (Node.js)', () => {
it('should export all expected resources', () => {
expect(typeof createInterceptorServer).toBe('function');
expectTypeOf<InterceptorServer>().not.toBeAny();
expectTypeOf<InterceptorServerOptions>().not.toBeAny();
expectTypeOf<NotStartedInterceptorServerError>().not.toBeAny();
expect(typeof NotStartedInterceptorServerError).toBe('function');
expect(typeof runCommand).toBe('function');
expectTypeOf<CommandError>().not.toBeAny();
expect(typeof CommandError).toBe('function');

expect(DEFAULT_ACCESS_CONTROL_HEADERS).toEqual(expect.any(Object));
expect(DEFAULT_PREFLIGHT_STATUS_CODE).toEqual(expect.any(Number));
});
});
3 changes: 1 addition & 2 deletions examples/with-next-js-app/.env.development
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
NODE_ENV=development

ZIMIC_SERVER_URL=http://localhost:3005
NEXT_PUBLIC_GITHUB_API_BASE_URL=$ZIMIC_SERVER_URL/github
GITHUB_API_BASE_URL=https://api.github.com
3 changes: 3 additions & 0 deletions examples/with-next-js-app/.env.test
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
NODE_ENV=test

ZIMIC_SERVER_URL=http://localhost:3005
GITHUB_API_BASE_URL=$ZIMIC_SERVER_URL/github
26 changes: 11 additions & 15 deletions examples/with-next-js-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,21 @@
Zimic + Next.js App Router
</h2>

This example uses Zimic with [Next.js](https://nextjs.org). The application is verified with end-to-end tests using
[Playwright](https://playwright.dev).
This example uses Zimic with [Next.js](https://nextjs.org).

## Application

The application is a simple [Next.js](https://nextjs.org) application using the
[App Router](https://nextjs.org/docs/app). It fetches repositories from the
[GitHub API](https://docs.github.com/en/rest).
The application is a simple [Next.js](https://nextjs.org) project using the [App Router](https://nextjs.org/docs/app).
It fetches repositories from the [GitHub API](https://docs.github.com/en/rest).

- Application: [`src/app/app/page.page.tsx`](./src/app/page.tsx)
- Interceptor provider:
[`src/providers/interceptors/InterceptorProvider.tsx`](./src/providers/interceptors/InterceptorProvider.tsx)
- This provider is used to apply Zimic mocks when the application is started in development.
- Application: [`src/app/page.tsx`](./src/app/page.tsx)
- GitHub fetch: [`src/services/github.ts`](./src/services/github.ts)
- Before fetching resources, it is necessary to wait for the interceptors and fixtures to be loaded. This is done via
`await waitForLoadedInterceptors();`.

A `postinstall` in [`package.json`](./package.json) script is used to install Playwright's browsers.
A `postinstall` script in [`package.json`](./package.json) is used to install Playwright's browsers.

The script [`tests/interceptors/scripts/load.ts`](./tests/interceptors/scripts/load.ts) loads the interceptors and mocks
before the application is started in development. It is used by the command `dev:mock` in
[`package.json`](./package.json).

## Testing

Expand All @@ -28,8 +25,7 @@ GitHub API and simulate a test case where the repository is found and another wh

### Zimic

- GitHub interceptor: [`tests/interceptors/github/interceptor.ts`](./tests/interceptors/github/interceptor.ts)
- Fixtures: [`tests/interceptors/github/fixtures.ts`](./tests/interceptors/github/fixtures.ts)
- GitHub interceptor and mocks: [`tests/interceptors/github.ts`](./tests/interceptors/github.ts)

### Test

Expand Down Expand Up @@ -62,7 +58,7 @@ GitHub API and simulate a test case where the repository is found and another wh
1. Start the application:

```bash
pnpm run dev
pnpm run dev:mock
```

After started, the application will be available at [http://localhost:3004](http://localhost:3004).
Expand Down
5 changes: 4 additions & 1 deletion examples/with-next-js-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"version": "0.0.0",
"private": false,
"scripts": {
"dev": "dotenv -c development -- zimic server start --port 3005 --ephemeral -- next dev --turbo --port 3004",
"dev": "dotenv -c development -- next dev --turbo --port 3004",
"dev:mock": "dotenv -c test -- zimic server start --port 3005 --ephemeral -- pnpm dev:load-interceptors -- pnpm dev",
"dev:load-interceptors": "tsx ./tests/interceptors/scripts/load.ts",
"test": "dotenv -c test -- dotenv -c development -- playwright test",
"test:turbo": "pnpm run test",
"types:check": "tsc --noEmit",
Expand All @@ -25,6 +27,7 @@
"@types/react-dom": "^18.3.0",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.1",
"tsx": "^4.7.0",
"typescript": "^5.4.3",
"zimic": "latest"
}
Expand Down
2 changes: 1 addition & 1 deletion examples/with-next-js-app/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default defineConfig({
],

webServer: {
command: 'pnpm run dev',
command: 'pnpm run dev:mock',
port: 3004,
stdout: 'pipe',
stderr: 'pipe',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test, { expect } from '@playwright/test';

import { githubFixtures } from '../../tests/interceptors/github/fixtures';
import { githubFixtures } from '../../tests/interceptors/github';

test.describe('Home page', () => {
const { repository } = githubFixtures;
Expand Down
9 changes: 2 additions & 7 deletions examples/with-next-js-app/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ import { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { PropsWithChildren } from 'react';

import { loadInterceptors } from '../../tests/interceptors';
import InterceptorProvider from '../providers/interceptors/InterceptorProvider';

import '../styles/global.css';

const inter = Inter({ subsets: ['latin'] });
Expand All @@ -15,13 +12,11 @@ export const metadata: Metadata = {
description: 'Generated by create-next-app',
};

async function RootLayout({ children }: PropsWithChildren) {
await loadInterceptors();

function RootLayout({ children }: PropsWithChildren) {
return (
<html lang="en">
<body className={clsx(inter.className, 'bg-slate-100 flex flex-col items-center justify-center min-h-screen')}>
<InterceptorProvider>{children}</InterceptorProvider>
{children}
</body>
</html>
);
Expand Down
2 changes: 1 addition & 1 deletion examples/with-next-js-app/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function HomePage({ searchParams }: Props) {
<GitHubRepositoryForm />

{shouldFetchRepository && (
<Suspense key={`${ownerName}-${repositoryName}`} fallback={<p role="status">Loading...</p>}>
<Suspense fallback={<p role="status">Loading...</p>}>
<GitHubRepositoryShowcase ownerName={ownerName} repositoryName={repositoryName} />
</Suspense>
)}
Expand Down
2 changes: 1 addition & 1 deletion examples/with-next-js-app/src/config/environment.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const environment = {
GITHUB_API_BASE_URL: process.env.NEXT_PUBLIC_GITHUB_API_BASE_URL ?? '',
GITHUB_API_BASE_URL: process.env.GITHUB_API_BASE_URL ?? '',
};

export default environment;

This file was deleted.

7 changes: 2 additions & 5 deletions examples/with-next-js-app/src/services/github.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type { JSONValue } from 'zimic';

import { waitForLoadedInterceptors } from '../../tests/interceptors';
import environment from '../config/environment';

const BASE_URL = environment.GITHUB_API_BASE_URL;
const GITHUB_API_BASE_URL = environment.GITHUB_API_BASE_URL;
const CACHE_STRATEGY = process.env.NODE_ENV === 'production' ? 'default' : 'no-store';

export type GitHubRepository = JSONValue<{
Expand All @@ -15,13 +14,11 @@ export type GitHubRepository = JSONValue<{
}>;

export async function fetchGitHubRepository(ownerName: string, repositoryName: string) {
await waitForLoadedInterceptors();

try {
const sanitizedOwnerName = encodeURIComponent(ownerName);
const sanitizedRepositoryName = encodeURIComponent(repositoryName);

const response = await fetch(`${BASE_URL}/repos/${sanitizedOwnerName}/${sanitizedRepositoryName}`, {
const response = await fetch(`${GITHUB_API_BASE_URL}/repos/${sanitizedOwnerName}/${sanitizedRepositoryName}`, {
cache: CACHE_STRATEGY,
});

Expand Down
43 changes: 43 additions & 0 deletions examples/with-next-js-app/tests/interceptors/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { http } from 'zimic/interceptor';

import environment from '../../src/config/environment';
import { GitHubRepository } from '../../src/services/github';

const githubInterceptor = http.createInterceptor<{
'/repos/:owner/:name': {
GET: {
response: {
200: { body: GitHubRepository };
404: { body: { message: string } };
500: { body: { message: string } };
};
};
};
}>({
type: 'remote',
baseURL: environment.GITHUB_API_BASE_URL,
});

export const githubFixtures = {
repository: {
id: 1,
name: 'example',
full_name: 'owner/example',
html_url: 'https://github.com/owner/example',
owner: { login: 'owner' },
} satisfies GitHubRepository,

async apply() {
await githubInterceptor.get('/repos/:owner/:name').respond({
status: 404,
body: { message: 'Not Found' },
});

await githubInterceptor.get(`/repos/${this.repository.owner.login}/${this.repository.name}`).respond({
status: 200,
body: this.repository,
});
},
};

export default githubInterceptor;
26 changes: 0 additions & 26 deletions examples/with-next-js-app/tests/interceptors/github/fixtures.ts

This file was deleted.

This file was deleted.

Loading
Loading