From 6da6de76e713f9c29fc21bb9db25a61f77c8d786 Mon Sep 17 00:00:00 2001 From: Diego Aquino Date: Sun, 26 May 2024 14:50:59 -0300 Subject: [PATCH 01/10] refactor(examples-next-js-pages): simplify setup --- .../src/__tests__/HomePage.e2e.test.ts | 2 +- .../src/pages/_app.page.tsx | 23 +++++++--- .../interceptors/InterceptorProvider.tsx | 15 ------- .../with-next-js-pages/src/services/github.ts | 7 +-- .../tests/interceptors/github.ts | 43 +++++++++++++++++++ .../tests/interceptors/github/fixtures.ts | 26 ----------- .../tests/interceptors/github/interceptor.ts | 21 --------- .../tests/interceptors/index.ts | 39 ----------------- .../tests/interceptors/utils.ts | 6 +++ 9 files changed, 68 insertions(+), 114 deletions(-) delete mode 100644 examples/with-next-js-pages/src/providers/interceptors/InterceptorProvider.tsx create mode 100644 examples/with-next-js-pages/tests/interceptors/github.ts delete mode 100644 examples/with-next-js-pages/tests/interceptors/github/fixtures.ts delete mode 100644 examples/with-next-js-pages/tests/interceptors/github/interceptor.ts delete mode 100644 examples/with-next-js-pages/tests/interceptors/index.ts create mode 100644 examples/with-next-js-pages/tests/interceptors/utils.ts diff --git a/examples/with-next-js-pages/src/__tests__/HomePage.e2e.test.ts b/examples/with-next-js-pages/src/__tests__/HomePage.e2e.test.ts index c71e1a47..4e731636 100644 --- a/examples/with-next-js-pages/src/__tests__/HomePage.e2e.test.ts +++ b/examples/with-next-js-pages/src/__tests__/HomePage.e2e.test.ts @@ -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; diff --git a/examples/with-next-js-pages/src/pages/_app.page.tsx b/examples/with-next-js-pages/src/pages/_app.page.tsx index a51fa8ce..7dc2d8a9 100644 --- a/examples/with-next-js-pages/src/pages/_app.page.tsx +++ b/examples/with-next-js-pages/src/pages/_app.page.tsx @@ -2,24 +2,33 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import clsx from 'clsx'; import type { AppProps } from 'next/app'; import { Inter } from 'next/font/google'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; -import InterceptorProvider from '../providers/interceptors/InterceptorProvider'; +import { loadInterceptors } from '../../tests/interceptors/utils'; import '../styles/global.css'; const inter = Inter({ subsets: ['latin'] }); +const SHOULD_LOAD_INTERCEPTORS = process.env.NODE_ENV !== 'production'; function App({ Component, pageProps }: AppProps) { const [queryClient] = useState(() => new QueryClient()); + const [isLoading, setIsLoading] = useState(SHOULD_LOAD_INTERCEPTORS); + + useEffect(() => { + if (SHOULD_LOAD_INTERCEPTORS) { + void (async () => { + await loadInterceptors(); + setIsLoading(false); + })(); + } + }, []); return ( - -
- -
-
+
+ {!isLoading && } +
); } diff --git a/examples/with-next-js-pages/src/providers/interceptors/InterceptorProvider.tsx b/examples/with-next-js-pages/src/providers/interceptors/InterceptorProvider.tsx deleted file mode 100644 index 82cbebda..00000000 --- a/examples/with-next-js-pages/src/providers/interceptors/InterceptorProvider.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { PropsWithChildren, useEffect } from 'react'; - -import { loadInterceptors } from '../../../tests/interceptors'; - -type Props = PropsWithChildren; - -function InterceptorProvider({ children }: Props) { - useEffect(() => { - void loadInterceptors(); - }, []); - - return children; -} - -export default InterceptorProvider; diff --git a/examples/with-next-js-pages/src/services/github.ts b/examples/with-next-js-pages/src/services/github.ts index ea1d5b24..31a5ea89 100644 --- a/examples/with-next-js-pages/src/services/github.ts +++ b/examples/with-next-js-pages/src/services/github.ts @@ -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<{ @@ -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, }); diff --git a/examples/with-next-js-pages/tests/interceptors/github.ts b/examples/with-next-js-pages/tests/interceptors/github.ts new file mode 100644 index 00000000..16da1a90 --- /dev/null +++ b/examples/with-next-js-pages/tests/interceptors/github.ts @@ -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: 'local', + 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, + + apply() { + githubInterceptor.get('/repos/:owner/:name').respond({ + status: 404, + body: { message: 'Not Found' }, + }); + + githubInterceptor.get(`/repos/${this.repository.owner.login}/${this.repository.name}`).respond({ + status: 200, + body: this.repository, + }); + }, +}; + +export default githubInterceptor; diff --git a/examples/with-next-js-pages/tests/interceptors/github/fixtures.ts b/examples/with-next-js-pages/tests/interceptors/github/fixtures.ts deleted file mode 100644 index 6332e3ec..00000000 --- a/examples/with-next-js-pages/tests/interceptors/github/fixtures.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { GitHubRepository } from '../../../src/services/github'; -import githubInterceptor from './interceptor'; - -export const githubFixtures = { - repository: { - id: 1, - name: 'example', - full_name: 'owner/example', - html_url: 'https://github.com/owner/example', - owner: { login: 'owner' }, - }, -} satisfies Record; - -export function applyGitHubFixtures() { - const { repository } = githubFixtures; - - githubInterceptor.get('/repos/:owner/:name').respond({ - status: 404, - body: { message: 'Not Found' }, - }); - - githubInterceptor.get(`/repos/${repository.owner.login}/${repository.name}`).respond({ - status: 200, - body: repository, - }); -} diff --git a/examples/with-next-js-pages/tests/interceptors/github/interceptor.ts b/examples/with-next-js-pages/tests/interceptors/github/interceptor.ts deleted file mode 100644 index a7c6ebce..00000000 --- a/examples/with-next-js-pages/tests/interceptors/github/interceptor.ts +++ /dev/null @@ -1,21 +0,0 @@ -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: 'local', - baseURL: environment.GITHUB_API_BASE_URL, -}); - -export default githubInterceptor; diff --git a/examples/with-next-js-pages/tests/interceptors/index.ts b/examples/with-next-js-pages/tests/interceptors/index.ts deleted file mode 100644 index ab60ec74..00000000 --- a/examples/with-next-js-pages/tests/interceptors/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { applyGitHubFixtures } from './github/fixtures'; -import githubInterceptor from './github/interceptor'; - -export let markInterceptorsAsLoaded: (() => void) | undefined; -let areInterceptorsLoaded = false; - -const loadInterceptorsPromise = new Promise((resolve) => { - markInterceptorsAsLoaded = () => { - areInterceptorsLoaded = true; - resolve(); - }; -}); - -export async function waitForLoadedInterceptors() { - if (process.env.NODE_ENV === 'production' || areInterceptorsLoaded) { - return; - } - - await loadInterceptorsPromise; -} - -export async function loadInterceptors() { - if (process.env.NODE_ENV === 'production' || areInterceptorsLoaded) { - return; - } - - await githubInterceptor.start(); - applyGitHubFixtures(); - - markInterceptorsAsLoaded?.(); -} - -export async function stopInterceptors() { - if (process.env.NODE_ENV === 'production') { - return; - } - - await githubInterceptor.stop(); -} diff --git a/examples/with-next-js-pages/tests/interceptors/utils.ts b/examples/with-next-js-pages/tests/interceptors/utils.ts new file mode 100644 index 00000000..5d6d2d5d --- /dev/null +++ b/examples/with-next-js-pages/tests/interceptors/utils.ts @@ -0,0 +1,6 @@ +import githubInterceptor, { githubFixtures } from './github'; + +export async function loadInterceptors() { + await githubInterceptor.start(); + githubFixtures.apply(); +} From d08f164a4052b6fb5f37582b1c555294afabadf4 Mon Sep 17 00:00:00 2001 From: Diego Aquino Date: Sun, 26 May 2024 15:00:47 -0300 Subject: [PATCH 02/10] refactor(examples-next-js-app): improve setup --- examples/with-next-js-app/.env.development | 3 +- examples/with-next-js-app/.env.test | 3 ++ examples/with-next-js-app/README.md | 2 +- examples/with-next-js-app/package.json | 5 ++- .../with-next-js-app/playwright.config.ts | 2 +- .../src/__tests__/HomePage.e2e.test.ts | 2 +- examples/with-next-js-app/src/app/layout.tsx | 9 +--- examples/with-next-js-app/src/app/page.tsx | 2 +- .../src/config/environment.ts | 2 +- .../interceptors/InterceptorProvider.tsx | 17 -------- .../with-next-js-app/src/services/github.ts | 7 +-- .../tests/interceptors/github.ts | 43 +++++++++++++++++++ .../tests/interceptors/github/fixtures.ts | 26 ----------- .../tests/interceptors/github/interceptor.ts | 21 --------- .../tests/interceptors/index.ts | 39 ----------------- .../tests/interceptors/scripts/load.ts | 23 ++++++++++ 16 files changed, 83 insertions(+), 123 deletions(-) delete mode 100644 examples/with-next-js-app/src/providers/interceptors/InterceptorProvider.tsx create mode 100644 examples/with-next-js-app/tests/interceptors/github.ts delete mode 100644 examples/with-next-js-app/tests/interceptors/github/fixtures.ts delete mode 100644 examples/with-next-js-app/tests/interceptors/github/interceptor.ts delete mode 100644 examples/with-next-js-app/tests/interceptors/index.ts create mode 100644 examples/with-next-js-app/tests/interceptors/scripts/load.ts diff --git a/examples/with-next-js-app/.env.development b/examples/with-next-js-app/.env.development index 937666de..a0b79abb 100644 --- a/examples/with-next-js-app/.env.development +++ b/examples/with-next-js-app/.env.development @@ -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 diff --git a/examples/with-next-js-app/.env.test b/examples/with-next-js-app/.env.test index 2c87534c..062bada0 100644 --- a/examples/with-next-js-app/.env.test +++ b/examples/with-next-js-app/.env.test @@ -1 +1,4 @@ NODE_ENV=test + +ZIMIC_SERVER_URL=http://localhost:3005 +GITHUB_API_BASE_URL=$ZIMIC_SERVER_URL/github diff --git a/examples/with-next-js-app/README.md b/examples/with-next-js-app/README.md index 00c16bbe..39de5406 100644 --- a/examples/with-next-js-app/README.md +++ b/examples/with-next-js-app/README.md @@ -62,7 +62,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). diff --git a/examples/with-next-js-app/package.json b/examples/with-next-js-app/package.json index cdff4192..e7f63288 100644 --- a/examples/with-next-js-app/package.json +++ b/examples/with-next-js-app/package.json @@ -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", @@ -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" } diff --git a/examples/with-next-js-app/playwright.config.ts b/examples/with-next-js-app/playwright.config.ts index 0b20d779..4d1022bc 100644 --- a/examples/with-next-js-app/playwright.config.ts +++ b/examples/with-next-js-app/playwright.config.ts @@ -33,7 +33,7 @@ export default defineConfig({ ], webServer: { - command: 'pnpm run dev', + command: 'pnpm run dev:mock', port: 3004, stdout: 'pipe', stderr: 'pipe', diff --git a/examples/with-next-js-app/src/__tests__/HomePage.e2e.test.ts b/examples/with-next-js-app/src/__tests__/HomePage.e2e.test.ts index c71e1a47..4e731636 100644 --- a/examples/with-next-js-app/src/__tests__/HomePage.e2e.test.ts +++ b/examples/with-next-js-app/src/__tests__/HomePage.e2e.test.ts @@ -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; diff --git a/examples/with-next-js-app/src/app/layout.tsx b/examples/with-next-js-app/src/app/layout.tsx index 58f9a5c1..1e6a44d1 100644 --- a/examples/with-next-js-app/src/app/layout.tsx +++ b/examples/with-next-js-app/src/app/layout.tsx @@ -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'] }); @@ -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 ( - {children} + {children} ); diff --git a/examples/with-next-js-app/src/app/page.tsx b/examples/with-next-js-app/src/app/page.tsx index f92e8c85..8d4ed0d7 100644 --- a/examples/with-next-js-app/src/app/page.tsx +++ b/examples/with-next-js-app/src/app/page.tsx @@ -22,7 +22,7 @@ function HomePage({ searchParams }: Props) { {shouldFetchRepository && ( - Loading...

}> + Loading...

}>
)} diff --git a/examples/with-next-js-app/src/config/environment.ts b/examples/with-next-js-app/src/config/environment.ts index 329f426c..a1dfc20b 100644 --- a/examples/with-next-js-app/src/config/environment.ts +++ b/examples/with-next-js-app/src/config/environment.ts @@ -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; diff --git a/examples/with-next-js-app/src/providers/interceptors/InterceptorProvider.tsx b/examples/with-next-js-app/src/providers/interceptors/InterceptorProvider.tsx deleted file mode 100644 index 257f4df8..00000000 --- a/examples/with-next-js-app/src/providers/interceptors/InterceptorProvider.tsx +++ /dev/null @@ -1,17 +0,0 @@ -'use client'; - -import { PropsWithChildren, useEffect } from 'react'; - -import { loadInterceptors } from '../../../tests/interceptors'; - -type Props = PropsWithChildren; - -function InterceptorProvider({ children }: Props) { - useEffect(() => { - void loadInterceptors(); - }, []); - - return children; -} - -export default InterceptorProvider; diff --git a/examples/with-next-js-app/src/services/github.ts b/examples/with-next-js-app/src/services/github.ts index ea1d5b24..31a5ea89 100644 --- a/examples/with-next-js-app/src/services/github.ts +++ b/examples/with-next-js-app/src/services/github.ts @@ -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<{ @@ -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, }); diff --git a/examples/with-next-js-app/tests/interceptors/github.ts b/examples/with-next-js-app/tests/interceptors/github.ts new file mode 100644 index 00000000..aa554a87 --- /dev/null +++ b/examples/with-next-js-app/tests/interceptors/github.ts @@ -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; diff --git a/examples/with-next-js-app/tests/interceptors/github/fixtures.ts b/examples/with-next-js-app/tests/interceptors/github/fixtures.ts deleted file mode 100644 index 66893fb7..00000000 --- a/examples/with-next-js-app/tests/interceptors/github/fixtures.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { GitHubRepository } from '../../../src/services/github'; -import githubInterceptor from './interceptor'; - -export const githubFixtures = { - repository: { - id: 1, - name: 'example', - full_name: 'owner/example', - html_url: 'https://github.com/owner/example', - owner: { login: 'owner' }, - }, -} satisfies Record; - -export async function applyGitHubFixtures() { - const { repository } = githubFixtures; - - await githubInterceptor.get('/repos/:owner/:name').respond({ - status: 404, - body: { message: 'Not Found' }, - }); - - await githubInterceptor.get(`/repos/${repository.owner.login}/${repository.name}`).respond({ - status: 200, - body: repository, - }); -} diff --git a/examples/with-next-js-app/tests/interceptors/github/interceptor.ts b/examples/with-next-js-app/tests/interceptors/github/interceptor.ts deleted file mode 100644 index 7e7cc41f..00000000 --- a/examples/with-next-js-app/tests/interceptors/github/interceptor.ts +++ /dev/null @@ -1,21 +0,0 @@ -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 default githubInterceptor; diff --git a/examples/with-next-js-app/tests/interceptors/index.ts b/examples/with-next-js-app/tests/interceptors/index.ts deleted file mode 100644 index 5a4d3d31..00000000 --- a/examples/with-next-js-app/tests/interceptors/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { applyGitHubFixtures } from './github/fixtures'; -import githubInterceptor from './github/interceptor'; - -export let markInterceptorsAsLoaded: (() => void) | undefined; -let areInterceptorsLoaded = false; - -const loadInterceptorsPromise = new Promise((resolve) => { - markInterceptorsAsLoaded = () => { - areInterceptorsLoaded = true; - resolve(); - }; -}); - -export async function waitForLoadedInterceptors() { - if (process.env.NODE_ENV === 'production' || areInterceptorsLoaded) { - return; - } - - await loadInterceptorsPromise; -} - -export async function loadInterceptors() { - if (process.env.NODE_ENV === 'production' || areInterceptorsLoaded) { - return; - } - - await githubInterceptor.start(); - await applyGitHubFixtures(); - - markInterceptorsAsLoaded?.(); -} - -export async function stopInterceptors() { - if (process.env.NODE_ENV === 'production') { - return; - } - - await githubInterceptor.stop(); -} diff --git a/examples/with-next-js-app/tests/interceptors/scripts/load.ts b/examples/with-next-js-app/tests/interceptors/scripts/load.ts new file mode 100644 index 00000000..519ca364 --- /dev/null +++ b/examples/with-next-js-app/tests/interceptors/scripts/load.ts @@ -0,0 +1,23 @@ +import { runCommand } from 'zimic/server'; + +import githubInterceptor, { githubFixtures } from '../github'; + +async function runOnReadyCommand() { + const commandDivisorIndex = process.argv.indexOf('--'); + if (commandDivisorIndex !== -1) { + const [command, ...commandArguments] = process.argv.slice(commandDivisorIndex + 1); + await runCommand(command, commandArguments); + } +} + +async function loadInterceptors() { + await githubInterceptor.start(); + await githubFixtures.apply(); + console.log('Interceptors loaded.'); + + await runOnReadyCommand(); + + await githubInterceptor.stop(); +} + +void loadInterceptors(); From 5031c51dc182d9c5abc062ea44fa6378fb55f9db Mon Sep 17 00:00:00 2001 From: Diego Aquino Date: Sun, 26 May 2024 15:03:00 -0300 Subject: [PATCH 03/10] refactor(examples-playwright): simplify setup --- examples/with-playwright/.env.development | 2 +- examples/with-playwright/.env.test | 2 +- examples/with-playwright/README.md | 2 +- examples/with-playwright/package.json | 8 ++-- examples/with-playwright/playwright.config.ts | 2 +- .../src/app/__tests__/HomePage.e2e.test.ts | 4 +- examples/with-playwright/src/app/page.tsx | 2 +- .../with-playwright/src/config/environment.ts | 2 +- .../with-playwright/src/services/github.ts | 4 +- .../tests/interceptors/github.ts | 43 +++++++++++++++++++ .../tests/interceptors/github/fixtures.ts | 26 ----------- .../tests/interceptors/github/interceptor.ts | 21 --------- .../tests/interceptors/scripts/load.ts | 23 ++++++++++ examples/with-playwright/tests/setup.ts | 13 ------ pnpm-lock.yaml | 15 +++---- 15 files changed, 87 insertions(+), 82 deletions(-) create mode 100644 examples/with-playwright/tests/interceptors/github.ts delete mode 100644 examples/with-playwright/tests/interceptors/github/fixtures.ts delete mode 100644 examples/with-playwright/tests/interceptors/github/interceptor.ts create mode 100644 examples/with-playwright/tests/interceptors/scripts/load.ts delete mode 100644 examples/with-playwright/tests/setup.ts diff --git a/examples/with-playwright/.env.development b/examples/with-playwright/.env.development index 5973dabb..a0b79abb 100644 --- a/examples/with-playwright/.env.development +++ b/examples/with-playwright/.env.development @@ -1,3 +1,3 @@ NODE_ENV=development -NEXT_PUBLIC_GITHUB_API_BASE_URL=https://api.github.com +GITHUB_API_BASE_URL=https://api.github.com diff --git a/examples/with-playwright/.env.test b/examples/with-playwright/.env.test index 1031b27b..fa1d900b 100644 --- a/examples/with-playwright/.env.test +++ b/examples/with-playwright/.env.test @@ -1,4 +1,4 @@ NODE_ENV=test ZIMIC_SERVER_URL=http://localhost:3003 -NEXT_PUBLIC_GITHUB_API_BASE_URL=$ZIMIC_SERVER_URL/github +GITHUB_API_BASE_URL=$ZIMIC_SERVER_URL/github diff --git a/examples/with-playwright/README.md b/examples/with-playwright/README.md index 2c2b5e3c..7054229b 100644 --- a/examples/with-playwright/README.md +++ b/examples/with-playwright/README.md @@ -62,7 +62,7 @@ GitHub API and simulate a test case where the repository is found and another wh 1. Start the application: ```bash - pnpm run dev:test + pnpm run dev:mock ``` After started, it will be available at [http://localhost:3002](http://localhost:3002). diff --git a/examples/with-playwright/package.json b/examples/with-playwright/package.json index 83af907d..3f7e1df9 100644 --- a/examples/with-playwright/package.json +++ b/examples/with-playwright/package.json @@ -3,9 +3,10 @@ "version": "0.0.0", "private": false, "scripts": { - "dev": "dotenv -c development -- next dev --port 3002", - "dev:test": "dotenv -c test -- pnpm dev", - "test": "dotenv -c test -- dotenv -c development -- zimic server start --port 3003 --ephemeral -- playwright test", + "dev": "dotenv -c development -- next dev --turbo --port 3002", + "dev:mock": "dotenv -c test -- zimic server start --port 3003 --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", "deps:install-playwright": "pnpm playwright install chromium", @@ -25,6 +26,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" } diff --git a/examples/with-playwright/playwright.config.ts b/examples/with-playwright/playwright.config.ts index 6975de5c..43814990 100644 --- a/examples/with-playwright/playwright.config.ts +++ b/examples/with-playwright/playwright.config.ts @@ -34,7 +34,7 @@ export default defineConfig({ ], webServer: { - command: 'pnpm run dev:test', + command: 'pnpm run dev:mock', url: 'http://localhost:3002', stdout: 'pipe', stderr: 'pipe', diff --git a/examples/with-playwright/src/app/__tests__/HomePage.e2e.test.ts b/examples/with-playwright/src/app/__tests__/HomePage.e2e.test.ts index 65b7e273..4d2b2b51 100644 --- a/examples/with-playwright/src/app/__tests__/HomePage.e2e.test.ts +++ b/examples/with-playwright/src/app/__tests__/HomePage.e2e.test.ts @@ -1,8 +1,6 @@ import test, { expect } from '@playwright/test'; -import { githubFixtures } from '../../../tests/interceptors/github/fixtures'; - -import '../../../tests/setup'; +import { githubFixtures } from '../../../tests/interceptors/github'; test.describe('Home page', () => { const { repository } = githubFixtures; diff --git a/examples/with-playwright/src/app/page.tsx b/examples/with-playwright/src/app/page.tsx index 1fa7b3d8..610adc15 100644 --- a/examples/with-playwright/src/app/page.tsx +++ b/examples/with-playwright/src/app/page.tsx @@ -19,7 +19,7 @@ function HomePage({ searchParams }: Props) { {shouldFetchRepository && ( - Loading...

}> + Loading...

}>
)} diff --git a/examples/with-playwright/src/config/environment.ts b/examples/with-playwright/src/config/environment.ts index 329f426c..a1dfc20b 100644 --- a/examples/with-playwright/src/config/environment.ts +++ b/examples/with-playwright/src/config/environment.ts @@ -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; diff --git a/examples/with-playwright/src/services/github.ts b/examples/with-playwright/src/services/github.ts index 025f7af9..f8a77170 100644 --- a/examples/with-playwright/src/services/github.ts +++ b/examples/with-playwright/src/services/github.ts @@ -3,7 +3,7 @@ import type { JSONValue } from 'zimic'; 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<{ @@ -19,7 +19,7 @@ export const fetchGitHubRepository = cache(async (ownerName: string, repositoryN 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, }); diff --git a/examples/with-playwright/tests/interceptors/github.ts b/examples/with-playwright/tests/interceptors/github.ts new file mode 100644 index 00000000..aa554a87 --- /dev/null +++ b/examples/with-playwright/tests/interceptors/github.ts @@ -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; diff --git a/examples/with-playwright/tests/interceptors/github/fixtures.ts b/examples/with-playwright/tests/interceptors/github/fixtures.ts deleted file mode 100644 index 66893fb7..00000000 --- a/examples/with-playwright/tests/interceptors/github/fixtures.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { GitHubRepository } from '../../../src/services/github'; -import githubInterceptor from './interceptor'; - -export const githubFixtures = { - repository: { - id: 1, - name: 'example', - full_name: 'owner/example', - html_url: 'https://github.com/owner/example', - owner: { login: 'owner' }, - }, -} satisfies Record; - -export async function applyGitHubFixtures() { - const { repository } = githubFixtures; - - await githubInterceptor.get('/repos/:owner/:name').respond({ - status: 404, - body: { message: 'Not Found' }, - }); - - await githubInterceptor.get(`/repos/${repository.owner.login}/${repository.name}`).respond({ - status: 200, - body: repository, - }); -} diff --git a/examples/with-playwright/tests/interceptors/github/interceptor.ts b/examples/with-playwright/tests/interceptors/github/interceptor.ts deleted file mode 100644 index 7e7cc41f..00000000 --- a/examples/with-playwright/tests/interceptors/github/interceptor.ts +++ /dev/null @@ -1,21 +0,0 @@ -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 default githubInterceptor; diff --git a/examples/with-playwright/tests/interceptors/scripts/load.ts b/examples/with-playwright/tests/interceptors/scripts/load.ts new file mode 100644 index 00000000..519ca364 --- /dev/null +++ b/examples/with-playwright/tests/interceptors/scripts/load.ts @@ -0,0 +1,23 @@ +import { runCommand } from 'zimic/server'; + +import githubInterceptor, { githubFixtures } from '../github'; + +async function runOnReadyCommand() { + const commandDivisorIndex = process.argv.indexOf('--'); + if (commandDivisorIndex !== -1) { + const [command, ...commandArguments] = process.argv.slice(commandDivisorIndex + 1); + await runCommand(command, commandArguments); + } +} + +async function loadInterceptors() { + await githubInterceptor.start(); + await githubFixtures.apply(); + console.log('Interceptors loaded.'); + + await runOnReadyCommand(); + + await githubInterceptor.stop(); +} + +void loadInterceptors(); diff --git a/examples/with-playwright/tests/setup.ts b/examples/with-playwright/tests/setup.ts deleted file mode 100644 index b85441e1..00000000 --- a/examples/with-playwright/tests/setup.ts +++ /dev/null @@ -1,13 +0,0 @@ -import test from '@playwright/test'; - -import { applyGitHubFixtures } from './interceptors/github/fixtures'; -import githubInterceptor from './interceptors/github/interceptor'; - -test.beforeAll(async () => { - await githubInterceptor.start(); - await applyGitHubFixtures(); -}); - -test.afterAll(async () => { - await githubInterceptor.stop(); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 647133ce..3fffd33a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -207,6 +207,9 @@ importers: tailwindcss: specifier: ^3.4.1 version: 3.4.3 + tsx: + specifier: ^4.7.0 + version: 4.7.0 typescript: specifier: ^5.4.3 version: 5.4.5 @@ -296,6 +299,9 @@ importers: tailwindcss: specifier: ^3.4.1 version: 3.4.3 + tsx: + specifier: ^4.7.0 + version: 4.7.0 typescript: specifier: ^5.4.3 version: 5.4.3 @@ -3071,9 +3077,6 @@ packages: resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} engines: {node: '>= 0.4'} - get-tsconfig@4.7.2: - resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} - get-tsconfig@4.7.3: resolution: {integrity: sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==} @@ -8086,10 +8089,6 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.2.4 - get-tsconfig@4.7.2: - dependencies: - resolve-pkg-maps: 1.0.0 - get-tsconfig@4.7.3: dependencies: resolve-pkg-maps: 1.0.0 @@ -10271,7 +10270,7 @@ snapshots: tsx@4.7.0: dependencies: esbuild: 0.19.11 - get-tsconfig: 4.7.2 + get-tsconfig: 4.7.3 optionalDependencies: fsevents: 2.3.3 From 9b558a65352158efc73b1c1c3241b3272530370e Mon Sep 17 00:00:00 2001 From: Diego Aquino Date: Sun, 26 May 2024 15:36:51 -0300 Subject: [PATCH 04/10] docs(#zimic): sync server start help message --- README.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3d31c8ef..811f811e 100644 --- a/README.md +++ b/README.md @@ -1807,13 +1807,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: From e4789406e59154979457e98a711894e0d591b8bb Mon Sep 17 00:00:00 2001 From: Diego Aquino Date: Sun, 26 May 2024 15:49:42 -0300 Subject: [PATCH 05/10] feat(#zimic): provide server exports --- README.md | 24 +++++++++++ .../v0/interceptor/exports/exports.test.ts | 22 ++++++++++ packages/zimic/package.json | 6 +++ packages/zimic/server.d.ts | 1 + .../src/cli/__tests__/server.cli.node.test.ts | 4 +- packages/zimic/src/cli/cli.ts | 1 - packages/zimic/src/cli/server/start.ts | 19 +++------ .../__tests__/HttpInterceptor.node.test.ts | 4 +- .../errors/NotStartedHttpInterceptorError.ts | 2 +- .../errors/UnknownHttpInterceptorPlatform.ts | 2 +- .../errors/UnknownHttpInterceptorTypeError.ts | 2 +- .../HttpinterceptorWorker.node.test.ts | 4 +- .../errors/UnregisteredServiceWorkerError.ts | 2 +- .../__tests__/HttpRequestHandler.node.test.ts | 4 +- .../errors/NoResponseDefinitionError.ts | 2 +- .../interceptor/server/InterceptorServer.ts | 20 ++++++---- .../__tests__/InterceptorServer.node.test.ts | 10 +++-- .../zimic/src/interceptor/server/constants.ts | 2 + .../NotStartedInterceptorServerError.ts | 8 ++-- .../zimic/src/interceptor/server/factory.ts | 15 +++++++ .../zimic/src/interceptor/server/index.ts | 11 +++++ .../src/interceptor/server/types/options.ts | 26 ++++++++++++ .../src/interceptor/server/types/public.ts | 40 +++++++++++++++++++ packages/zimic/src/utils/http.ts | 4 +- packages/zimic/src/utils/processes.ts | 18 ++++++--- packages/zimic/src/utils/urls.ts | 6 +-- packages/zimic/src/utils/webSocket.ts | 6 +-- .../errors/InvalidWebSocketMessage.ts | 2 +- .../errors/NotStartedWebSocketHandlerError.ts | 2 +- .../zimic/tests/utils/interceptorServers.ts | 7 ++++ packages/zimic/tsup.config.ts | 1 + 31 files changed, 217 insertions(+), 60 deletions(-) create mode 100644 packages/zimic/server.d.ts create mode 100644 packages/zimic/src/interceptor/server/factory.ts create mode 100644 packages/zimic/src/interceptor/server/index.ts create mode 100644 packages/zimic/src/interceptor/server/types/options.ts create mode 100644 packages/zimic/tests/utils/interceptorServers.ts diff --git a/README.md b/README.md index 811f811e..af289aae 100644 --- a/README.md +++ b/README.md @@ -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 @@ -1839,6 +1840,29 @@ 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 also an option. + +```ts +import { createInterceptorServer, runCommand } from 'zimic/server'; + +const server = createInterceptorServer({ hostname: 'localhost', port: 3000 }); + +await server.start(); +console.log(server.isRunning()); // true + +const [command, ...commandArguments] = process.argv.slice(3); +await runCommand(command, commandArguments); + +await server.stop(); +``` + +The export `runCommand` is a helper function that runs a command with its arguments. 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 loading the interceptors and fixtures. + --- ## Changelog diff --git a/apps/zimic-test-client/tests/v0/interceptor/exports/exports.test.ts b/apps/zimic-test-client/tests/v0/interceptor/exports/exports.test.ts index 6563204c..8e78a991 100644 --- a/apps/zimic-test-client/tests/v0/interceptor/exports/exports.test.ts +++ b/apps/zimic-test-client/tests/v0/interceptor/exports/exports.test.ts @@ -64,6 +64,16 @@ import { NotStartedHttpInterceptorError, UnregisteredServiceWorkerError, } from 'zimic0/interceptor'; +import { + createInterceptorServer, + InterceptorServer, + InterceptorServerOptions, + NotStartedInterceptorServerError, + runCommand, + CommandError, + DEFAULT_ACCESS_CONTROL_HEADERS, + DEFAULT_PREFLIGHT_STATUS_CODE, +} from 'zimic0/server'; describe('Exports', () => { it('should export all expected resources', () => { @@ -162,5 +172,17 @@ describe('Exports', () => { expect(typeof NotStartedHttpInterceptorError).toBe('function'); expectTypeOf().not.toBeAny(); expect(typeof UnregisteredServiceWorkerError).toBe('function'); + + expect(typeof createInterceptorServer).toBe('function'); + expectTypeOf().not.toBeAny(); + expectTypeOf().not.toBeAny(); + expectTypeOf().not.toBeAny(); + expect(typeof NotStartedInterceptorServerError).toBe('function'); + expect(typeof runCommand).toBe('function'); + expectTypeOf().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)); }); }); diff --git a/packages/zimic/package.json b/packages/zimic/package.json index b14dceb4..5b58ca36 100644 --- a/packages/zimic/package.json +++ b/packages/zimic/package.json @@ -52,6 +52,12 @@ "default": "./dist/interceptor.js", "types": "./dist/interceptor.d.ts" }, + "./server": { + "import": "./dist/server.mjs", + "require": "./dist/server.js", + "default": "./dist/server.js", + "types": "./dist/server.d.ts" + }, "./package.json": "./package.json" }, "scripts": { diff --git a/packages/zimic/server.d.ts b/packages/zimic/server.d.ts new file mode 100644 index 00000000..699fd532 --- /dev/null +++ b/packages/zimic/server.d.ts @@ -0,0 +1 @@ +export * from './dist/server'; diff --git a/packages/zimic/src/cli/__tests__/server.cli.node.test.ts b/packages/zimic/src/cli/__tests__/server.cli.node.test.ts index 890cc255..4e607b82 100644 --- a/packages/zimic/src/cli/__tests__/server.cli.node.test.ts +++ b/packages/zimic/src/cli/__tests__/server.cli.node.test.ts @@ -373,7 +373,7 @@ describe('CLI (server)', async () => { await usingIgnoredConsole(['error', 'log'], async (spies) => { const error = new CommandError('node', exitCode, null); await expect(runCLI()).rejects.toThrowError(error); - expect(error.message).toBe(`[zimic] Command 'node' exited with code ${exitCode}.`); + expect(error.message).toBe(`Command 'node' exited with code ${exitCode}.`); expect(spies.error).toHaveBeenCalledTimes(1); expect(spies.error).toHaveBeenCalledWith(error); @@ -398,7 +398,7 @@ describe('CLI (server)', async () => { await usingIgnoredConsole(['error', 'log'], async (spies) => { const error = new CommandError('node', null, signal); await expect(runCLI()).rejects.toThrowError(error); - expect(error.message).toBe(`[zimic] Command 'node' exited after signal ${signal}.`); + expect(error.message).toBe(`Command 'node' exited after signal ${signal}.`); expect(spies.error).toHaveBeenCalledTimes(1); expect(spies.error).toHaveBeenCalledWith(error); diff --git a/packages/zimic/src/cli/cli.ts b/packages/zimic/src/cli/cli.ts index be9e5c82..7ab2f1e1 100644 --- a/packages/zimic/src/cli/cli.ts +++ b/packages/zimic/src/cli/cli.ts @@ -39,7 +39,6 @@ async function runCLI() { .positional('onReady', { description: 'A command to run when the server is ready to accept connections.', type: 'string', - array: true, }) .option('hostname', { type: 'string', diff --git a/packages/zimic/src/cli/server/start.ts b/packages/zimic/src/cli/server/start.ts index d6cf0e7b..584057e2 100644 --- a/packages/zimic/src/cli/server/start.ts +++ b/packages/zimic/src/cli/server/start.ts @@ -1,7 +1,8 @@ +import { createInterceptorServer } from '@/interceptor/server'; +import { InterceptorServerOptions } from '@/interceptor/server/types/options'; +import { InterceptorServer } from '@/interceptor/server/types/public'; import { logWithPrefix } from '@/utils/console'; -import { runCommand, PROCESS_EXIT_EVENTS } from '@/utils/processes'; - -import InterceptorServer, { InterceptorServerOptions } from '../../interceptor/server/InterceptorServer'; +import { runCommand } from '@/utils/processes'; interface InterceptorServerStartOptions extends InterceptorServerOptions { ephemeral: boolean; @@ -20,7 +21,7 @@ async function startInterceptorServer({ onUnhandledRequest, onReady, }: InterceptorServerStartOptions) { - const server = new InterceptorServer({ + const server = createInterceptorServer({ hostname, port, onUnhandledRequest, @@ -28,12 +29,6 @@ async function startInterceptorServer({ singletonServer = server; - for (const exitEvent of PROCESS_EXIT_EVENTS) { - process.on(exitEvent, async () => { - await server.stop(); - }); - } - await server.start(); logWithPrefix(`${ephemeral ? 'Ephemeral s' : 'S'}erver is running on ${server.httpURL()}`); @@ -45,10 +40,6 @@ async function startInterceptorServer({ if (ephemeral) { await server.stop(); } - - for (const exitEvent of PROCESS_EXIT_EVENTS) { - process.removeAllListeners(exitEvent); - } } export default startInterceptorServer; diff --git a/packages/zimic/src/interceptor/http/interceptor/__tests__/HttpInterceptor.node.test.ts b/packages/zimic/src/interceptor/http/interceptor/__tests__/HttpInterceptor.node.test.ts index a49980a5..a4e5e827 100644 --- a/packages/zimic/src/interceptor/http/interceptor/__tests__/HttpInterceptor.node.test.ts +++ b/packages/zimic/src/interceptor/http/interceptor/__tests__/HttpInterceptor.node.test.ts @@ -1,12 +1,12 @@ import { describe } from 'vitest'; -import InterceptorServer from '@/interceptor/server/InterceptorServer'; import { getNodeBaseURL } from '@tests/utils/interceptors'; +import { createInternalInterceptorServer } from '@tests/utils/interceptorServers'; import { declareSharedHttpInterceptorTests } from './shared/interceptorTests'; describe('HttpInterceptor (Node.js)', () => { - const server = new InterceptorServer(); + const server = createInternalInterceptorServer(); declareSharedHttpInterceptorTests({ platform: 'node', diff --git a/packages/zimic/src/interceptor/http/interceptor/errors/NotStartedHttpInterceptorError.ts b/packages/zimic/src/interceptor/http/interceptor/errors/NotStartedHttpInterceptorError.ts index c11b9316..08aa8972 100644 --- a/packages/zimic/src/interceptor/http/interceptor/errors/NotStartedHttpInterceptorError.ts +++ b/packages/zimic/src/interceptor/http/interceptor/errors/NotStartedHttpInterceptorError.ts @@ -7,7 +7,7 @@ */ class NotStartedHttpInterceptorError extends Error { constructor() { - super('[zimic] Interceptor is not running. Did you forget to call `await interceptor.start()`?'); + super('Interceptor is not running. Did you forget to call `await interceptor.start()`?'); this.name = 'NotStartedHttpInterceptorError'; } } diff --git a/packages/zimic/src/interceptor/http/interceptor/errors/UnknownHttpInterceptorPlatform.ts b/packages/zimic/src/interceptor/http/interceptor/errors/UnknownHttpInterceptorPlatform.ts index fa4c6eb6..2901cfc9 100644 --- a/packages/zimic/src/interceptor/http/interceptor/errors/UnknownHttpInterceptorPlatform.ts +++ b/packages/zimic/src/interceptor/http/interceptor/errors/UnknownHttpInterceptorPlatform.ts @@ -8,7 +8,7 @@ class UnknownHttpInterceptorPlatform extends Error { /* istanbul ignore next -- @preserve * Ignoring because checking unknown platforms is currently not possible in our Vitest setup */ constructor() { - super('[zimic] Unknown interceptor platform.'); + super('Unknown interceptor platform.'); this.name = 'UnknownHttpInterceptorPlatform'; } } diff --git a/packages/zimic/src/interceptor/http/interceptor/errors/UnknownHttpInterceptorTypeError.ts b/packages/zimic/src/interceptor/http/interceptor/errors/UnknownHttpInterceptorTypeError.ts index 9e3a3ae6..2aa73a9d 100644 --- a/packages/zimic/src/interceptor/http/interceptor/errors/UnknownHttpInterceptorTypeError.ts +++ b/packages/zimic/src/interceptor/http/interceptor/errors/UnknownHttpInterceptorTypeError.ts @@ -3,7 +3,7 @@ import { HttpInterceptorType } from '../types/options'; class UnknownHttpInterceptorTypeError extends TypeError { constructor(unknownType: unknown) { super( - `[zimic] Unknown HTTP interceptor type: ${unknownType}. The available options are ` + + `Unknown HTTP interceptor type: ${unknownType}. The available options are ` + `'${'local' satisfies HttpInterceptorType}' and ` + `'${'remote' satisfies HttpInterceptorType}'.`, ); diff --git a/packages/zimic/src/interceptor/http/interceptorWorker/__tests__/HttpinterceptorWorker.node.test.ts b/packages/zimic/src/interceptor/http/interceptorWorker/__tests__/HttpinterceptorWorker.node.test.ts index 66084cb1..4ed19c74 100644 --- a/packages/zimic/src/interceptor/http/interceptorWorker/__tests__/HttpinterceptorWorker.node.test.ts +++ b/packages/zimic/src/interceptor/http/interceptorWorker/__tests__/HttpinterceptorWorker.node.test.ts @@ -1,12 +1,12 @@ import { describe } from 'vitest'; -import InterceptorServer from '@/interceptor/server/InterceptorServer'; import { getNodeBaseURL } from '@tests/utils/interceptors'; +import { createInternalInterceptorServer } from '@tests/utils/interceptorServers'; import { declareSharedHttpInterceptorWorkerTests } from './shared/workerTests'; describe('HttpInterceptorWorker (Node.js)', () => { - const server = new InterceptorServer(); + const server = createInternalInterceptorServer(); declareSharedHttpInterceptorWorkerTests({ platform: 'node', diff --git a/packages/zimic/src/interceptor/http/interceptorWorker/errors/UnregisteredServiceWorkerError.ts b/packages/zimic/src/interceptor/http/interceptorWorker/errors/UnregisteredServiceWorkerError.ts index 1d98f1e9..052bd96a 100644 --- a/packages/zimic/src/interceptor/http/interceptorWorker/errors/UnregisteredServiceWorkerError.ts +++ b/packages/zimic/src/interceptor/http/interceptorWorker/errors/UnregisteredServiceWorkerError.ts @@ -8,7 +8,7 @@ import { SERVICE_WORKER_FILE_NAME } from '@/cli/browser/shared/constants'; class UnregisteredServiceWorkerError extends Error { constructor() { super( - `[zimic] Failed to register the browser service worker: ` + + `Failed to register the browser service worker: ` + `script '${window.location.origin}/${SERVICE_WORKER_FILE_NAME}' not found.\n\n` + 'Did you forget to run "npx zimic browser init "?\n\n' + 'Learn more at https://github.com/zimicjs/zimic#browser-post-install.', diff --git a/packages/zimic/src/interceptor/http/requestHandler/__tests__/HttpRequestHandler.node.test.ts b/packages/zimic/src/interceptor/http/requestHandler/__tests__/HttpRequestHandler.node.test.ts index c14d457a..d9676feb 100644 --- a/packages/zimic/src/interceptor/http/requestHandler/__tests__/HttpRequestHandler.node.test.ts +++ b/packages/zimic/src/interceptor/http/requestHandler/__tests__/HttpRequestHandler.node.test.ts @@ -1,12 +1,12 @@ import { describe } from 'vitest'; -import InterceptorServer from '@/interceptor/server/InterceptorServer'; import { getNodeBaseURL } from '@tests/utils/interceptors'; +import { createInternalInterceptorServer } from '@tests/utils/interceptorServers'; import { declareSharedHttpRequestHandlerTests } from './shared/requestHandlerTests'; describe('HttpRequestHandler (Node.js)', () => { - const server = new InterceptorServer(); + const server = createInternalInterceptorServer(); declareSharedHttpRequestHandlerTests({ platform: 'node', diff --git a/packages/zimic/src/interceptor/http/requestHandler/errors/NoResponseDefinitionError.ts b/packages/zimic/src/interceptor/http/requestHandler/errors/NoResponseDefinitionError.ts index 53f20c53..74aedf4b 100644 --- a/packages/zimic/src/interceptor/http/requestHandler/errors/NoResponseDefinitionError.ts +++ b/packages/zimic/src/interceptor/http/requestHandler/errors/NoResponseDefinitionError.ts @@ -1,6 +1,6 @@ class NoResponseDefinitionError extends TypeError { constructor() { - super('[zimic] Cannot generate a response without a definition. Use .respond() to set a response.'); + super('Cannot generate a response without a definition. Use .respond() to set a response.'); this.name = 'NoResponseDefinitionError'; } } diff --git a/packages/zimic/src/interceptor/server/InterceptorServer.ts b/packages/zimic/src/interceptor/server/InterceptorServer.ts index d2f06db2..87340412 100644 --- a/packages/zimic/src/interceptor/server/InterceptorServer.ts +++ b/packages/zimic/src/interceptor/server/InterceptorServer.ts @@ -8,23 +8,17 @@ import HttpInterceptorWorker from '@/interceptor/http/interceptorWorker/HttpInte import HttpInterceptorWorkerStore from '@/interceptor/http/interceptorWorker/HttpInterceptorWorkerStore'; import { deserializeResponse, serializeRequest } from '@/utils/fetch'; import { getHttpServerPort, startHttpServer, stopHttpServer } from '@/utils/http'; +import { PROCESS_EXIT_EVENTS } from '@/utils/processes'; import { createRegexFromURL, createURL, excludeNonPathParams } from '@/utils/urls'; import { WebSocket } from '@/webSocket/types'; import WebSocketServer from '@/webSocket/WebSocketServer'; import { DEFAULT_ACCESS_CONTROL_HEADERS, DEFAULT_PREFLIGHT_STATUS_CODE } from './constants'; import NotStartedInterceptorServerError from './errors/NotStartedInterceptorServerError'; +import { InterceptorServerOptions } from './types/options'; import { InterceptorServer as PublicInterceptorServer } from './types/public'; import { HttpHandlerCommit, InterceptorServerWebSocketSchema } from './types/schema'; -export interface InterceptorServerOptions { - hostname?: string; - port?: number; - onUnhandledRequest?: { - log?: boolean; - }; -} - interface HttpHandler { id: string; url: { regex: RegExp }; @@ -103,6 +97,12 @@ class InterceptorServer implements PublicInterceptorServer { return; } + for (const exitEvent of PROCESS_EXIT_EVENTS) { + process.on(exitEvent, async () => { + await this.stop(); + }); + } + this._httpServer = createServer({ keepAlive: true, joinDuplicateHeaders: true, @@ -199,6 +199,10 @@ class InterceptorServer implements PublicInterceptorServer { await this.stopWebSocketServer(); await this.stopHttpServer(); + + for (const exitEvent of PROCESS_EXIT_EVENTS) { + process.removeAllListeners(exitEvent); + } } private async stopHttpServer() { diff --git a/packages/zimic/src/interceptor/server/__tests__/InterceptorServer.node.test.ts b/packages/zimic/src/interceptor/server/__tests__/InterceptorServer.node.test.ts index 01f092c1..b15184e9 100644 --- a/packages/zimic/src/interceptor/server/__tests__/InterceptorServer.node.test.ts +++ b/packages/zimic/src/interceptor/server/__tests__/InterceptorServer.node.test.ts @@ -1,5 +1,7 @@ import { afterEach, describe, expect, it } from 'vitest'; +import { createInternalInterceptorServer } from '@tests/utils/interceptorServers'; + import InterceptorServer from '../InterceptorServer'; // These are integration tests for the server. Only features not easily reproducible by the CLI and the remote @@ -13,7 +15,7 @@ describe('Interceptor server', () => { }); it('should start correctly with a defined port', async () => { - server = new InterceptorServer({ hostname: 'localhost', port: 8080 }); + server = createInternalInterceptorServer({ hostname: 'localhost', port: 8080 }); expect(server.isRunning()).toBe(false); expect(server.hostname()).toBe('localhost'); @@ -29,7 +31,7 @@ describe('Interceptor server', () => { }); it('should start correctly with an undefined port', async () => { - server = new InterceptorServer({ hostname: 'localhost' }); + server = createInternalInterceptorServer({ hostname: 'localhost' }); expect(server.isRunning()).toBe(false); expect(server.hostname()).toBe('localhost'); @@ -45,7 +47,7 @@ describe('Interceptor server', () => { }); it('should not throw an error is started multiple times', async () => { - server = new InterceptorServer({ hostname: 'localhost' }); + server = createInternalInterceptorServer({ hostname: 'localhost' }); expect(server.isRunning()).toBe(false); @@ -60,7 +62,7 @@ describe('Interceptor server', () => { }); it('should not throw an error if stopped multiple times', async () => { - server = new InterceptorServer({ hostname: 'localhost' }); + server = createInternalInterceptorServer({ hostname: 'localhost' }); expect(server.isRunning()).toBe(false); diff --git a/packages/zimic/src/interceptor/server/constants.ts b/packages/zimic/src/interceptor/server/constants.ts index 1e00c52d..99b77375 100644 --- a/packages/zimic/src/interceptor/server/constants.ts +++ b/packages/zimic/src/interceptor/server/constants.ts @@ -15,6 +15,7 @@ export type AccessControlHeaders = HttpSchema.Headers<{ 'access-control-max-age'?: string; }>; +/** The default access control headers for the server. */ export const DEFAULT_ACCESS_CONTROL_HEADERS = Object.freeze({ 'access-control-allow-origin': '*', 'access-control-allow-methods': ALLOWED_ACCESS_CONTROL_HTTP_METHODS, @@ -23,4 +24,5 @@ export const DEFAULT_ACCESS_CONTROL_HEADERS = Object.freeze({ 'access-control-max-age': process.env.SERVER_ACCESS_CONTROL_MAX_AGE, }) satisfies AccessControlHeaders; +/** The default status code for the preflight request. */ export const DEFAULT_PREFLIGHT_STATUS_CODE = 204; diff --git a/packages/zimic/src/interceptor/server/errors/NotStartedInterceptorServerError.ts b/packages/zimic/src/interceptor/server/errors/NotStartedInterceptorServerError.ts index d3a3e7c8..578939f8 100644 --- a/packages/zimic/src/interceptor/server/errors/NotStartedInterceptorServerError.ts +++ b/packages/zimic/src/interceptor/server/errors/NotStartedInterceptorServerError.ts @@ -1,10 +1,8 @@ -/* istanbul ignore next -- @preserve - * This error is a fallback to prevent doing operations without a started server. It should not happen in normal - * conditions. - */ +/* istanbul ignore next -- @preserve */ +/** An error thrown when the interceptor server is not running. */ class NotStartedInterceptorServerError extends Error { constructor() { - super('[zimic] The interceptor server is not running.'); + super('The interceptor server is not running.'); this.name = 'NotStartedInterceptorServerError'; } } diff --git a/packages/zimic/src/interceptor/server/factory.ts b/packages/zimic/src/interceptor/server/factory.ts new file mode 100644 index 00000000..190dc7dd --- /dev/null +++ b/packages/zimic/src/interceptor/server/factory.ts @@ -0,0 +1,15 @@ +import InterceptorServer from './InterceptorServer'; +import { InterceptorServerOptions } from './types/options'; +import { InterceptorServer as PublicInterceptorServer } from './types/public'; + +/** + * Creates an {@link https://github.com/zimicjs/zimic#zimic-server interceptor server}. + * + * @param options The options to create the server. + * @returns The created server. + * @see {@link https://github.com/zimicjs/zimic#zimic-server `zimic server` API reference} + * @see {@link https://github.com/zimicjs/zimic#remote-http-interceptors Remote HTTP Interceptors} . + */ +export function createInterceptorServer(options?: InterceptorServerOptions): PublicInterceptorServer { + return new InterceptorServer(options); +} diff --git a/packages/zimic/src/interceptor/server/index.ts b/packages/zimic/src/interceptor/server/index.ts new file mode 100644 index 00000000..e4ee9834 --- /dev/null +++ b/packages/zimic/src/interceptor/server/index.ts @@ -0,0 +1,11 @@ +import NotStartedInterceptorServerError from './errors/NotStartedInterceptorServerError'; + +export type { InterceptorServerOptions } from './types/options'; +export type { InterceptorServer } from './types/public'; + +export { DEFAULT_ACCESS_CONTROL_HEADERS, DEFAULT_PREFLIGHT_STATUS_CODE } from './constants'; + +export { createInterceptorServer } from './factory'; +export { NotStartedInterceptorServerError }; + +export { runCommand, CommandError } from '@/utils/processes'; diff --git a/packages/zimic/src/interceptor/server/types/options.ts b/packages/zimic/src/interceptor/server/types/options.ts new file mode 100644 index 00000000..f87eea0c --- /dev/null +++ b/packages/zimic/src/interceptor/server/types/options.ts @@ -0,0 +1,26 @@ +/** + * The options to create an {@link https://github.com/zimicjs/zimic#zimic-server interceptor server}. + * + * @see {@link https://github.com/zimicjs/zimic#zimic-server `zimic server` API reference} + */ +export interface InterceptorServerOptions { + /** + * The hostname to start the server on. + * + * @default localhost + */ + hostname?: string; + + /** The port to start the server on. If no port is provided, a random one is chosen. */ + port?: number; + + /** The strategy to handle unhandled requests. */ + onUnhandledRequest?: { + /** + * Whether to log unhandled requests. + * + * @default true + */ + log?: boolean; + }; +} diff --git a/packages/zimic/src/interceptor/server/types/public.ts b/packages/zimic/src/interceptor/server/types/public.ts index 59fe529d..203bba61 100644 --- a/packages/zimic/src/interceptor/server/types/public.ts +++ b/packages/zimic/src/interceptor/server/types/public.ts @@ -1,9 +1,49 @@ +/** + * A server to intercept and handle requests. It is used in combination with + * {@link https://github.com/zimicjs/zimic#remote-http-interceptors remote interceptors}. + * + * @see {@link https://github.com/zimicjs/zimic#zimic-server `zimic server` API reference} + */ export interface InterceptorServer { + /** + * The hostname of the server. + * + * @see {@link https://github.com/zimicjs/zimic#zimic-server `zimic server` API reference} + */ hostname: () => string; + + /** + * The port of the server. + * + * @see {@link https://github.com/zimicjs/zimic#zimic-server `zimic server` API reference} + */ port: () => number | undefined; + + /** + * The HTTP URL of the server. + * + * @see {@link https://github.com/zimicjs/zimic#zimic-server `zimic server` API reference} + */ httpURL: () => string | undefined; + + /** + * Whether the server is running. + * + * @see {@link https://github.com/zimicjs/zimic#zimic-server `zimic server` API reference} + */ isRunning: () => boolean; + /** + * Start the server. + * + * @see {@link https://github.com/zimicjs/zimic#zimic-server `zimic server` API reference} + */ start: () => Promise; + + /** + * Stop the server. + * + * @see {@link https://github.com/zimicjs/zimic#zimic-server `zimic server` API reference} + */ stop: () => Promise; } diff --git a/packages/zimic/src/utils/http.ts b/packages/zimic/src/utils/http.ts index 07670e89..6fe21cd6 100644 --- a/packages/zimic/src/utils/http.ts +++ b/packages/zimic/src/utils/http.ts @@ -4,14 +4,14 @@ class HttpServerTimeoutError extends Error {} export class HttpServerStartTimeoutError extends HttpServerTimeoutError { constructor(reachedTimeout: number) { - super(`[zimic] HTTP server start timed out after ${reachedTimeout}ms.`); + super(`HTTP server start timed out after ${reachedTimeout}ms.`); this.name = 'HttpServerStartTimeout'; } } export class HttpServerStopTimeoutError extends HttpServerTimeoutError { constructor(reachedTimeout: number) { - super(`[zimic] HTTP server stop timed out after ${reachedTimeout}ms.`); + super(`HTTP server stop timed out after ${reachedTimeout}ms.`); this.name = 'HttpServerStopTimeout'; } } diff --git a/packages/zimic/src/utils/processes.ts b/packages/zimic/src/utils/processes.ts index c2e190af..960edc27 100644 --- a/packages/zimic/src/utils/processes.ts +++ b/packages/zimic/src/utils/processes.ts @@ -1,3 +1,4 @@ +import { SpawnOptions } from 'child_process'; import { spawn } from 'cross-spawn'; export const PROCESS_EXIT_EVENTS = Object.freeze([ @@ -9,20 +10,27 @@ export const PROCESS_EXIT_EVENTS = Object.freeze([ 'SIGBREAK', ] as const); +/** An error thrown when a command exits with a non-zero code. */ export class CommandError extends Error { constructor(command: string, exitCode: number | null, signal: NodeJS.Signals | null) { - super( - `[zimic] Command '${command}' exited ` + - `${exitCode === null ? `after signal ${signal}` : `with code ${exitCode}`}.`, - ); + super(`Command '${command}' exited ${exitCode === null ? `after signal ${signal}` : `with code ${exitCode}`}.`); this.name = 'CommandError'; } } -export async function runCommand(command: string, commandArguments: string[]) { +/** + * Runs a command with the given arguments. + * + * @param command The command to run. + * @param commandArguments The arguments to pass to the command. + * @param options The options to pass to the spawn function. By default, stdio is set to 'inherit'. + * @throws {CommandError} When the command exits with a non-zero code. + */ +export async function runCommand(command: string, commandArguments: string[], options: SpawnOptions = {}) { await new Promise((resolve, reject) => { const childProcess = spawn(command, commandArguments, { stdio: 'inherit', + ...options, }); childProcess.once('error', (error) => { diff --git a/packages/zimic/src/utils/urls.ts b/packages/zimic/src/utils/urls.ts index b981794a..00e978e1 100644 --- a/packages/zimic/src/utils/urls.ts +++ b/packages/zimic/src/utils/urls.ts @@ -1,6 +1,6 @@ export class InvalidURLError extends TypeError { constructor(url: unknown) { - super(`[zimic] Invalid URL: '${url}'`); + super(`Invalid URL: '${url}'`); this.name = 'InvalidURL'; } } @@ -8,7 +8,7 @@ export class InvalidURLError extends TypeError { export class UnsupportedURLProtocolError extends TypeError { constructor(protocol: string, availableProtocols: string[] | readonly string[]) { super( - `[zimic] Unsupported URL protocol: '${protocol}'. ` + + `Unsupported URL protocol: '${protocol}'. ` + `The available options are ${availableProtocols.map((protocol) => `'${protocol}'`).join(', ')}`, ); this.name = 'UnsupportedURLProtocolError'; @@ -62,7 +62,7 @@ export function excludeNonPathParams(url: URL) { export class DuplicatedPathParamError extends Error { constructor(url: string, paramName: string) { super( - `[zimic] The path parameter '${paramName}' appears more than once in the URL '${url}'. This is not supported. ` + + `The path parameter '${paramName}' appears more than once in the URL '${url}'. This is not supported. ` + 'Please make sure that each parameter is unique.', ); this.name = 'DuplicatedPathParamError'; diff --git a/packages/zimic/src/utils/webSocket.ts b/packages/zimic/src/utils/webSocket.ts index a9ed4069..8e9a4476 100644 --- a/packages/zimic/src/utils/webSocket.ts +++ b/packages/zimic/src/utils/webSocket.ts @@ -6,21 +6,21 @@ class WebSocketTimeoutError extends Error {} export class WebSocketOpenTimeoutError extends WebSocketTimeoutError { constructor(reachedTimeout: number) { - super(`[zimic] Web socket open timed out after ${reachedTimeout}ms.`); + super(`Web socket open timed out after ${reachedTimeout}ms.`); this.name = 'WebSocketOpenTimeout'; } } export class WebSocketMessageTimeoutError extends WebSocketTimeoutError { constructor(reachedTimeout: number) { - super(`[zimic] Web socket message timed out after ${reachedTimeout}ms.`); + super(`Web socket message timed out after ${reachedTimeout}ms.`); this.name = 'WebSocketMessageTimeout'; } } export class WebSocketCloseTimeoutError extends WebSocketTimeoutError { constructor(reachedTimeout: number) { - super(`[zimic] Web socket close timed out after ${reachedTimeout}ms.`); + super(`Web socket close timed out after ${reachedTimeout}ms.`); this.name = 'WebSocketCloseTimeout'; } } diff --git a/packages/zimic/src/webSocket/errors/InvalidWebSocketMessage.ts b/packages/zimic/src/webSocket/errors/InvalidWebSocketMessage.ts index 57b01222..07837ac0 100644 --- a/packages/zimic/src/webSocket/errors/InvalidWebSocketMessage.ts +++ b/packages/zimic/src/webSocket/errors/InvalidWebSocketMessage.ts @@ -1,6 +1,6 @@ class InvalidWebSocketMessage extends Error { constructor(message: unknown) { - super(`[zimic] Web socket message is invalid and could not be parsed: ${message}`); + super(`Web socket message is invalid and could not be parsed: ${message}`); this.name = 'InvalidWebSocketMessage'; } } diff --git a/packages/zimic/src/webSocket/errors/NotStartedWebSocketHandlerError.ts b/packages/zimic/src/webSocket/errors/NotStartedWebSocketHandlerError.ts index d8b31a69..fd346228 100644 --- a/packages/zimic/src/webSocket/errors/NotStartedWebSocketHandlerError.ts +++ b/packages/zimic/src/webSocket/errors/NotStartedWebSocketHandlerError.ts @@ -1,6 +1,6 @@ class NotStartedWebSocketHandlerError extends Error { constructor() { - super('[zimic] Web socket handler is not running.'); + super('Web socket handler is not running.'); this.name = 'NotStartedWebSocketHandlerError'; } } diff --git a/packages/zimic/tests/utils/interceptorServers.ts b/packages/zimic/tests/utils/interceptorServers.ts new file mode 100644 index 00000000..15156fc3 --- /dev/null +++ b/packages/zimic/tests/utils/interceptorServers.ts @@ -0,0 +1,7 @@ +import { InterceptorServerOptions, createInterceptorServer } from '@/interceptor/server'; +import InterceptorServer from '@/interceptor/server/InterceptorServer'; +import { InterceptorServer as PublicInterceptorServer } from '@/interceptor/server/types/public'; + +export function createInternalInterceptorServer(options?: InterceptorServerOptions) { + return createInterceptorServer(options) satisfies PublicInterceptorServer as InterceptorServer; +} diff --git a/packages/zimic/tsup.config.ts b/packages/zimic/tsup.config.ts index aafba563..b969fad9 100644 --- a/packages/zimic/tsup.config.ts +++ b/packages/zimic/tsup.config.ts @@ -22,6 +22,7 @@ export default defineConfig([ entry: { index: 'src/index.ts', interceptor: 'src/interceptor/index.ts', + server: 'src/interceptor/server/index.ts', }, }, { From b424744c55f9b2b6bf1a827449e005c6cb8469e2 Mon Sep 17 00:00:00 2001 From: Diego Aquino Date: Sun, 26 May 2024 15:55:04 -0300 Subject: [PATCH 06/10] test(zimic-test-client): split shared and server-only exports --- .../interceptor/exports/exports.node.test.ts | 27 +++++++++++++++++++ .../v0/interceptor/exports/exports.test.ts | 22 --------------- 2 files changed, 27 insertions(+), 22 deletions(-) create mode 100644 apps/zimic-test-client/tests/v0/interceptor/exports/exports.node.test.ts diff --git a/apps/zimic-test-client/tests/v0/interceptor/exports/exports.node.test.ts b/apps/zimic-test-client/tests/v0/interceptor/exports/exports.node.test.ts new file mode 100644 index 00000000..36c30e93 --- /dev/null +++ b/apps/zimic-test-client/tests/v0/interceptor/exports/exports.node.test.ts @@ -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().not.toBeAny(); + expectTypeOf().not.toBeAny(); + expectTypeOf().not.toBeAny(); + expect(typeof NotStartedInterceptorServerError).toBe('function'); + expect(typeof runCommand).toBe('function'); + expectTypeOf().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)); + }); +}); diff --git a/apps/zimic-test-client/tests/v0/interceptor/exports/exports.test.ts b/apps/zimic-test-client/tests/v0/interceptor/exports/exports.test.ts index 8e78a991..6563204c 100644 --- a/apps/zimic-test-client/tests/v0/interceptor/exports/exports.test.ts +++ b/apps/zimic-test-client/tests/v0/interceptor/exports/exports.test.ts @@ -64,16 +64,6 @@ import { NotStartedHttpInterceptorError, UnregisteredServiceWorkerError, } from 'zimic0/interceptor'; -import { - createInterceptorServer, - InterceptorServer, - InterceptorServerOptions, - NotStartedInterceptorServerError, - runCommand, - CommandError, - DEFAULT_ACCESS_CONTROL_HEADERS, - DEFAULT_PREFLIGHT_STATUS_CODE, -} from 'zimic0/server'; describe('Exports', () => { it('should export all expected resources', () => { @@ -172,17 +162,5 @@ describe('Exports', () => { expect(typeof NotStartedHttpInterceptorError).toBe('function'); expectTypeOf().not.toBeAny(); expect(typeof UnregisteredServiceWorkerError).toBe('function'); - - expect(typeof createInterceptorServer).toBe('function'); - expectTypeOf().not.toBeAny(); - expectTypeOf().not.toBeAny(); - expectTypeOf().not.toBeAny(); - expect(typeof NotStartedInterceptorServerError).toBe('function'); - expect(typeof runCommand).toBe('function'); - expectTypeOf().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)); }); }); From 46cdfc54aad0ccd8fb1bc7d95a25d731a8583763 Mon Sep 17 00:00:00 2001 From: Diego Aquino Date: Sun, 26 May 2024 17:00:27 -0300 Subject: [PATCH 07/10] docs(examples): update `README.md` with simplified setup --- examples/with-next-js-app/README.md | 24 ++++++++++-------------- examples/with-next-js-pages/README.md | 18 +++++------------- examples/with-playwright/README.md | 18 +++++++----------- examples/with-vitest-browser/README.md | 2 +- 4 files changed, 23 insertions(+), 39 deletions(-) diff --git a/examples/with-next-js-app/README.md b/examples/with-next-js-app/README.md index 39de5406..f78cd00c 100644 --- a/examples/with-next-js-app/README.md +++ b/examples/with-next-js-app/README.md @@ -2,24 +2,21 @@ Zimic + Next.js App Router -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 +fixtures before the application is started in development. It is included in the command `dev:mock` in +[`package.json`](./package.json). ## Testing @@ -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 fixtures: [`tests/interceptors/github.ts`](./tests/interceptors/github.ts) ### Test diff --git a/examples/with-next-js-pages/README.md b/examples/with-next-js-pages/README.md index fa59aa8d..c0070add 100644 --- a/examples/with-next-js-pages/README.md +++ b/examples/with-next-js-pages/README.md @@ -2,26 +2,19 @@ Zimic + Next.js Pages Router -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 +The application is a simple [Next.js](https://nextjs.org) project using the [Pages Router](https://nextjs.org/docs/pages). It fetches repositories from the [GitHub API](https://docs.github.com/en/rest). - Application: [`src/pages/index.page.tsx`](./src/pages/index.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. - 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 and initialize -Zimic's mock service worker to the `./public` directory. The mock service worker at `./public/mockServiceWorker.js` is -ignored in the [`.gitignore`](./.gitignore) file. +The file [`_app.page.tsx`](./src/pages/_app.page.tsx) loads the interceptors and fixtures before the rest of the +application is rendered in development. ## Testing @@ -30,8 +23,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 fixtures: [`tests/interceptors/github.ts`](./tests/interceptors/github.ts) ### Test diff --git a/examples/with-playwright/README.md b/examples/with-playwright/README.md index 7054229b..3ead339c 100644 --- a/examples/with-playwright/README.md +++ b/examples/with-playwright/README.md @@ -6,13 +6,17 @@ This example uses Zimic with [Playwright](https://playwright.dev) in end-to-end ## Application -The tested application is a simple [Next.js](https://nextjs.org) application, fetching repositories from the +The tested application is a simple [Next.js](https://nextjs.org) project, fetching repositories from the [GitHub API](https://docs.github.com/en/rest). - Application: [`src/app/page.tsx`](./src/app/page.tsx) - GitHub fetch: [`src/services/github.ts`](./src/services/github.ts) -A `postinstall` in [`package.json`](./package.json#L12) 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 +fixtures before the application is started in development. It is included in the command `dev:mock` in +[`package.json`](./package.json). ## Testing @@ -21,21 +25,13 @@ 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 fixtures: [`tests/interceptors/github.ts`](./tests/interceptors/github.ts) ### Test - Test suite: [`src/app/__tests__/HomePage.e2e.test.ts`](./src/app/__tests__/HomePage.e2e.test.ts) -- Test setup file: [`tests/setup.ts`](./tests/setup.ts) - - This file is responsible for starting the Zimic interceptors before each test. It also applies default mock - responses based on the [fixtures](./tests/interceptors/github/interceptor.ts). - Playwright configuration: [`playwright.config.ts`](./playwright.config.ts) -> [!IMPORTANT] -> -> The setup file must be imported from each test file to apply the global `test.beforeAll` and `test.afterAll`. - ### Running 1. Clone this example: diff --git a/examples/with-vitest-browser/README.md b/examples/with-vitest-browser/README.md index 60571b13..4c35012d 100644 --- a/examples/with-vitest-browser/README.md +++ b/examples/with-vitest-browser/README.md @@ -13,7 +13,7 @@ The application is a simple HTML layout rendered by vanilla JavaScript, fetching - Application: [`src/app.ts`](./src/app.ts) -A `postinstall` in [`package.json`](./package.json) script is used to install Playwright's browsers and initialize +A `postinstall` script in [`package.json`](./package.json) is used to install Playwright's browsers and initialize Zimic's mock service worker to the `./public` directory. The mock service worker at `./public/mockServiceWorker.js` is ignored in the [`.gitignore`](./.gitignore) file. From 74523c51910e79f036f7ac4297b75d172c5300d2 Mon Sep 17 00:00:00 2001 From: Diego Aquino Date: Sun, 26 May 2024 18:11:17 -0300 Subject: [PATCH 08/10] docs(examples): add explanation about parallelism in playwright --- examples/with-next-js-app/README.md | 6 +++--- examples/with-next-js-pages/README.md | 4 ++-- examples/with-playwright/README.md | 21 ++++++++++++++++++--- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/examples/with-next-js-app/README.md b/examples/with-next-js-app/README.md index f78cd00c..7d16a676 100644 --- a/examples/with-next-js-app/README.md +++ b/examples/with-next-js-app/README.md @@ -14,8 +14,8 @@ It fetches repositories from the [GitHub API](https://docs.github.com/en/rest). 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 -fixtures before the application is started in development. It is included in the command `dev:mock` in +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 @@ -25,7 +25,7 @@ GitHub API and simulate a test case where the repository is found and another wh ### Zimic -- GitHub interceptor and fixtures: [`tests/interceptors/github.ts`](./tests/interceptors/github.ts) +- GitHub interceptor and mocks: [`tests/interceptors/github.ts`](./tests/interceptors/github.ts) ### Test diff --git a/examples/with-next-js-pages/README.md b/examples/with-next-js-pages/README.md index c0070add..51123109 100644 --- a/examples/with-next-js-pages/README.md +++ b/examples/with-next-js-pages/README.md @@ -13,7 +13,7 @@ The application is a simple [Next.js](https://nextjs.org) project using the - Application: [`src/pages/index.page.tsx`](./src/pages/index.page.tsx) - GitHub fetch: [`src/services/github.ts`](./src/services/github.ts) -The file [`_app.page.tsx`](./src/pages/_app.page.tsx) loads the interceptors and fixtures before the rest of the +The file [`_app.page.tsx`](./src/pages/_app.page.tsx) loads the interceptors and mocks before the rest of the application is rendered in development. ## Testing @@ -23,7 +23,7 @@ GitHub API and simulate a test case where the repository is found and another wh ### Zimic -- GitHub interceptor and fixtures: [`tests/interceptors/github.ts`](./tests/interceptors/github.ts) +- GitHub interceptor and mocks: [`tests/interceptors/github.ts`](./tests/interceptors/github.ts) ### Test diff --git a/examples/with-playwright/README.md b/examples/with-playwright/README.md index 3ead339c..aff92ef1 100644 --- a/examples/with-playwright/README.md +++ b/examples/with-playwright/README.md @@ -14,10 +14,25 @@ The tested application is a simple [Next.js](https://nextjs.org) project, fetchi 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 -fixtures before the application is started in development. It is included in the command `dev:mock` in +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). +> [!NOTE] +> +> **Preventing racing conditions** +> +> The mocks are loaded before starting the application to prevent racing conditions in tests. This example uses a single +> interceptor server, so we would need to reduce the number of workers to 1 if the mocks were applied inside the tests +> or `beforeEach`/`beforeAll`/`afterEach`/`afterAll` hooks. That would make the tests significantly slower in large +> applications, which is a trade-off to consider. +> +> If using a single test worker is not a problem for your project, applying the mocks inside your tests or hooks is +> perfectly possible. On the other hand, if you need parallelism, you can still simulate dynamic behavior by creating +> all of the mocks you need beforehand in a load script like in this example. Using +> [restrictions](https://github.com/zimicjs/zimic#http-handlerwithrestriction) is a good way to narrow down the scope of +> those mocks. + ## Testing An example test suite uses [Playwright](https://playwright.dev) to test the application. Zimic is used to mock the @@ -25,7 +40,7 @@ GitHub API and simulate a test case where the repository is found and another wh ### Zimic -- GitHub interceptor and fixtures: [`tests/interceptors/github.ts`](./tests/interceptors/github.ts) +- GitHub interceptor and mocks: [`tests/interceptors/github.ts`](./tests/interceptors/github.ts) ### Test From 79ef6ddf418cb728414b24f12ffa02519c927749 Mon Sep 17 00:00:00 2001 From: Diego Aquino Date: Sun, 26 May 2024 18:27:37 -0300 Subject: [PATCH 09/10] refactor(#zimic): clear only own server process listeners --- .../interceptor/server/InterceptorServer.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/zimic/src/interceptor/server/InterceptorServer.ts b/packages/zimic/src/interceptor/server/InterceptorServer.ts index 87340412..0c9b7840 100644 --- a/packages/zimic/src/interceptor/server/InterceptorServer.ts +++ b/packages/zimic/src/interceptor/server/InterceptorServer.ts @@ -49,7 +49,7 @@ class InterceptorServer implements PublicInterceptorServer { private knownWorkerSockets = new Set(); - constructor(options: InterceptorServerOptions = {}) { + constructor(options: InterceptorServerOptions) { this._hostname = options.hostname ?? 'localhost'; this._port = options.port; this.onUnhandledRequest = options.onUnhandledRequest; @@ -98,9 +98,7 @@ class InterceptorServer implements PublicInterceptorServer { } for (const exitEvent of PROCESS_EXIT_EVENTS) { - process.on(exitEvent, async () => { - await this.stop(); - }); + process.on(exitEvent, this.stop); } this._httpServer = createServer({ @@ -192,18 +190,18 @@ class InterceptorServer implements PublicInterceptorServer { } } - async stop() { + stop = async () => { if (!this.isRunning()) { return; } - await this.stopWebSocketServer(); - await this.stopHttpServer(); - for (const exitEvent of PROCESS_EXIT_EVENTS) { - process.removeAllListeners(exitEvent); + process.removeListener(exitEvent, this.stop); } - } + + await this.stopWebSocketServer(); + await this.stopHttpServer(); + }; private async stopHttpServer() { const httpServer = this.httpServer(); From e3ebe11e747b196ae9c7ac4ee2016441e28c7a98 Mon Sep 17 00:00:00 2001 From: Diego Aquino Date: Sun, 26 May 2024 18:27:54 -0300 Subject: [PATCH 10/10] docs(#zimic): improve documentation --- README.md | 9 ++++----- .../zimic/src/interceptor/server/factory.ts | 2 +- .../src/interceptor/server/types/public.ts | 19 +++++++++---------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index af289aae..990a3234 100644 --- a/README.md +++ b/README.md @@ -1843,25 +1843,24 @@ 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 also an option. +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(); -console.log(server.isRunning()); // true +// Run a command when the server is ready const [command, ...commandArguments] = process.argv.slice(3); await runCommand(command, commandArguments); await server.stop(); ``` -The export `runCommand` is a helper function that runs a command with its arguments. The +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 loading the interceptors and fixtures. +this function to run the application after the interceptor server is ready and all mocks are set up. --- diff --git a/packages/zimic/src/interceptor/server/factory.ts b/packages/zimic/src/interceptor/server/factory.ts index 190dc7dd..9196e11e 100644 --- a/packages/zimic/src/interceptor/server/factory.ts +++ b/packages/zimic/src/interceptor/server/factory.ts @@ -10,6 +10,6 @@ import { InterceptorServer as PublicInterceptorServer } from './types/public'; * @see {@link https://github.com/zimicjs/zimic#zimic-server `zimic server` API reference} * @see {@link https://github.com/zimicjs/zimic#remote-http-interceptors Remote HTTP Interceptors} . */ -export function createInterceptorServer(options?: InterceptorServerOptions): PublicInterceptorServer { +export function createInterceptorServer(options: InterceptorServerOptions = {}): PublicInterceptorServer { return new InterceptorServer(options); } diff --git a/packages/zimic/src/interceptor/server/types/public.ts b/packages/zimic/src/interceptor/server/types/public.ts index 203bba61..cc9ee36b 100644 --- a/packages/zimic/src/interceptor/server/types/public.ts +++ b/packages/zimic/src/interceptor/server/types/public.ts @@ -6,42 +6,41 @@ */ export interface InterceptorServer { /** - * The hostname of the server. - * + * @returns The hostname of the server. * @see {@link https://github.com/zimicjs/zimic#zimic-server `zimic server` API reference} */ hostname: () => string; /** - * The port of the server. - * + * @returns The port of the server. * @see {@link https://github.com/zimicjs/zimic#zimic-server `zimic server` API reference} */ port: () => number | undefined; /** - * The HTTP URL of the server. - * + * @returns The HTTP URL of the server. * @see {@link https://github.com/zimicjs/zimic#zimic-server `zimic server` API reference} */ httpURL: () => string | undefined; /** - * Whether the server is running. - * + * @returns Whether the server is running. * @see {@link https://github.com/zimicjs/zimic#zimic-server `zimic server` API reference} */ isRunning: () => boolean; /** - * Start the server. + * Starts the server. + * + * The server is automatically stopped if a process exit event is detected, such as SIGINT, SIGTERM, or an uncaught + * exception. * * @see {@link https://github.com/zimicjs/zimic#zimic-server `zimic server` API reference} */ start: () => Promise; /** - * Stop the server. + * Stops the server. * * @see {@link https://github.com/zimicjs/zimic#zimic-server `zimic server` API reference} */