diff --git a/.github/workflows/test-Windows.yml b/.github/workflows/test-Windows.yml index 9da84f0..b3f279c 100644 --- a/.github/workflows/test-Windows.yml +++ b/.github/workflows/test-Windows.yml @@ -40,11 +40,11 @@ jobs: - name: Install Dependencies run: pnpm install - - name: Build Packages - run: pnpm run build - - name: Build Docs run: pnpm run docs:build - name: Unit Test run: pnpm run test + + - name: E2E Test + run: pnpm playwright install --with-deps chromium && pnpm run e2e \ No newline at end of file diff --git a/.github/workflows/test-macOS.yml b/.github/workflows/test-macOS.yml index afd4435..2f9d503 100644 --- a/.github/workflows/test-macOS.yml +++ b/.github/workflows/test-macOS.yml @@ -40,11 +40,11 @@ jobs: - name: Install Dependencies run: pnpm install - - name: Build Packages - run: pnpm run build - - name: Build Docs run: pnpm run docs:build - name: Unit Test run: pnpm run test + + - name: E2E Test + run: pnpm playwright install --with-deps chromium && pnpm run e2e \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9fdd811..a2d9fec 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,7 @@ doc_build .TODO .env -backup \ No newline at end of file +backup + +playwright-report +test-results diff --git a/AGENTS.md b/AGENTS.md index deb1f8c..8cdda97 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,12 +11,30 @@ This file describes the `rspress-plugins` project, the tools and frameworks it u This project utilizes the following tools and frameworks: * **Package Manager:** [pnpm](https://pnpm.io/) -* **Testing:** [Vitest](https://vitest.dev/) +* **Testing:** [Vitest](https://vitest.dev/) (Unit), [Playwright](https://playwright.dev/) (E2E) * **Language:** [TypeScript](https://www.typescriptlang.org/) * **Linting & Formatting:** [ESLint](https://eslint.org/) and [Prettier](https://prettier.io/) * **Versioning:** [Changesets](https://github.com/changesets/changesets) * **UI Framework:** [React](https://reactjs.org/) +## Setup commands + +- Install deps: `pnpm install` + +- Start dev server: `pnpm dev` + +- Run unit tests: `pnpm test` + +- Run e2e tests: `pnpm e2e` + +## Code style + +- TypeScript strict mode + +- Single quotes, no semicolons + +- Use functional patterns where possible + ## Content from rspress.rs The following content was retrieved from [https://v2.rspress.rs/llms.txt](https://v2.rspress.rs/llms.txt): diff --git a/e2e/utils.ts b/e2e/utils.ts new file mode 100644 index 0000000..d60b8f6 --- /dev/null +++ b/e2e/utils.ts @@ -0,0 +1,134 @@ +import { ChildProcess } from 'child_process'; +import spawn from 'cross-spawn'; +import getPortLib from 'get-port'; +import treeKill from 'tree-kill'; + +export const getRandomPort = async () => { + return getPortLib(); +}; + +export const killProcess = async (instance: ChildProcess) => { + return new Promise((resolve, reject) => { + if (!instance || !instance.pid) { + resolve(null); + return; + } + treeKill(instance.pid, (err) => { + if (err) { + if ( + process.platform === 'win32' && + typeof err.message === 'string' && + (err.message.includes('no running instance of the task') || + err.message.includes('not found')) + ) { + // Windows throws an error if the process is already dead + return resolve(null); + } + return reject(err); + } + return resolve(null); + }); + }); +}; + +export const runDevCommand = async ( + root: string, + port?: number, +): Promise<{ process: ChildProcess; url: string }> => { + const targetPort = port || (await getRandomPort()); + const childProcess = spawn('pnpm', ['rspress', 'dev', '--port', targetPort.toString()], { + cwd: root, + stdio: 'pipe', + env: { + ...process.env, + NODE_ENV: 'development', + }, + }); + + return new Promise((resolve, reject) => { + let resolved = false; + childProcess.stdout?.on('data', (data) => { + const output = data.toString(); + // Rspress dev server started + if (output.includes('http://localhost') && !resolved) { + resolved = true; + resolve({ process: childProcess, url: `http://localhost:${targetPort}` }); + } + }); + + childProcess.stderr?.on('data', (data) => { + console.error(`Dev Error: ${data}`); + }); + + childProcess.on('error', (err) => { + reject(err); + }); + + childProcess.on('close', (code) => { + if (!resolved && code !== 0) { + reject(new Error(`Dev process exited with code ${code}`)); + } + }); + }); +}; + +export const runBuildCommand = async (root: string) => { + return new Promise((resolve, reject) => { + const childProcess = spawn('pnpm', ['rspress', 'build'], { + cwd: root, + stdio: 'ignore', + env: { + ...process.env, + NODE_ENV: 'production', + }, + }); + + childProcess.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Build failed with code ${code}`)); + } + }); + }); +}; + +export const runPreviewCommand = async ( + root: string, + port?: number, +): Promise<{ process: ChildProcess; url: string }> => { + const targetPort = port || (await getRandomPort()); + const childProcess = spawn('npx', ['rspress', 'preview', '--port', targetPort.toString()], { + cwd: root, + stdio: 'pipe', + env: { + ...process.env, + NODE_ENV: 'production', + }, + }); + + return new Promise((resolve, reject) => { + let resolved = false; + childProcess.stdout?.on('data', (data) => { + const output = data.toString(); + if (output.includes('http://localhost') && !resolved) { + resolved = true; + resolve({ process: childProcess, url: `http://localhost:${targetPort}` }); + } + }); + + childProcess.stderr?.on('data', (data) => { + console.error(`Preview Error: ${data}`); + }); + + childProcess.on('error', (err) => { + reject(err); + }); + + childProcess.on('close', (code) => { + if (!resolved && code !== 0) { + reject(new Error(`Preview process exited with code ${code}`)); + } + }); + }); +}; diff --git a/package.json b/package.json index ccc86cc..fb190bf 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,12 @@ "name": "rspress-plugins", "version": "0.0.0", "private": true, + "type": "module", "workspaces": [ "packages/*" ], "scripts": { + "prepare": "npm run packages:build", "clean": "rm -rf ./packages/**/{dist,build}", "build": "pnpm run clean && pnpm packages:build", "commit": "git add -A && czg", @@ -14,6 +16,7 @@ "docs:build": "pnpm -r --filter=./packages/**/* run docs:build", "test": "vitest", "testu": "vitest --update", + "e2e": "playwright test", "test:cov": "vitest --coverage", "version": "changeset version && pnpm install --frozen-lockfile", "version:beta": "changeset pre enter beta && changeset version", @@ -27,6 +30,8 @@ }, "devDependencies": { "@changesets/cli": "^2.27.1", + "@playwright/test": "^1.57.0", + "@types/cross-spawn": "^6.0.6", "@types/fs-extra": "^11.0.4", "@types/lodash": "^4.17.0", "@types/pacote": "^11.1.8", @@ -36,17 +41,21 @@ "clipboardy": "^4.0.0", "clsx": "^2.1.0", "consola": "^3.2.3", + "cross-spawn": "^7.0.6", "czg": "^1.9.1", "eslint": "^9.0.0", "execa": "^8.0.1", + "get-port": "^7.1.0", "lodash": "^4.17.21", "npm-run-all": "^4.1.5", "p-try": "^3.0.0", + "playwright": "^1.57.0", "prettier": "^3.2.5", "react": "^18.2.0", "react-dom": "^18.2.0", "rimraf": "^5.0.5", "stylelint": "^16.3.1", + "tree-kill": "^1.2.2", "typescript": "^5.4.4", "util-ts-types": "^1.0.0", "vitest": "1.4.0" diff --git a/packages/rspress-plugin-file-tree/tests/parser.spec.ts b/packages/rspress-plugin-file-tree/tests/parser.test.ts similarity index 100% rename from packages/rspress-plugin-file-tree/tests/parser.spec.ts rename to packages/rspress-plugin-file-tree/tests/parser.test.ts diff --git a/packages/rspress-plugin-katex/index.spec.ts b/packages/rspress-plugin-katex/index.spec.ts new file mode 100644 index 0000000..8cceed2 --- /dev/null +++ b/packages/rspress-plugin-katex/index.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { runDevCommand, killProcess } from '../../e2e/utils.ts'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +test.describe('rspress-plugin-katex', () => { + let devProcess: any; + let url: string; + + test.beforeAll(async () => { + const result = await runDevCommand(__dirname); + devProcess = result.process; + url = result.url; + }); + + test.afterAll(async () => { + if (devProcess) { + await killProcess(devProcess); + } + }); + + test('should render katex math', async ({ page }) => { + await page.goto(url, { waitUntil: 'networkidle' }); + // Check if katex-html element exists, which is generated by katex + const katexElement = page.locator('.katex-html'); + await expect(katexElement).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/packages/rspress-plugin-katex/vitest.config.ts b/packages/rspress-plugin-katex/vitest.config.ts new file mode 100644 index 0000000..7b7f814 --- /dev/null +++ b/packages/rspress-plugin-katex/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + exclude: ['**/*.spec.ts', 'node_modules'], + }, +}); diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..7f9c98b --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testMatch: 'packages/**/*.spec.ts', + // 30 min for all tests + globalTimeout: 30 * 60 * 1000, + // 1 min for each test + timeout: 60 * 1000, + quiet: true, + reporter: 'list', + use: { + trace: 'on', + video: 'on', + viewport: { width: 1440, height: 900 }, // screen size + }, + retries: process.env.CI ? 3 : 0, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 218d578..294447a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: '@changesets/cli': specifier: ^2.27.1 version: 2.27.1 + '@playwright/test': + specifier: ^1.57.0 + version: 1.57.0 + '@types/cross-spawn': + specifier: ^6.0.6 + version: 6.0.6 '@types/fs-extra': specifier: ^11.0.4 version: 11.0.4 @@ -38,6 +44,9 @@ importers: consola: specifier: ^3.2.3 version: 3.2.3 + cross-spawn: + specifier: ^7.0.6 + version: 7.0.6 czg: specifier: ^1.9.1 version: 1.9.1 @@ -47,6 +56,9 @@ importers: execa: specifier: ^8.0.1 version: 8.0.1 + get-port: + specifier: ^7.1.0 + version: 7.1.0 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -56,6 +68,9 @@ importers: p-try: specifier: ^3.0.0 version: 3.0.0 + playwright: + specifier: ^1.57.0 + version: 1.57.0 prettier: specifier: ^3.2.5 version: 3.2.5 @@ -71,6 +86,9 @@ importers: stylelint: specifier: ^16.3.1 version: 16.3.1(typescript@5.4.4) + tree-kill: + specifier: ^1.2.2 + version: 1.2.2 typescript: specifier: ^5.4.4 version: 5.4.4 @@ -1354,6 +1372,14 @@ packages: dev: true optional: true + /@playwright/test@1.57.0: + resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==} + engines: {node: '>=18'} + hasBin: true + dependencies: + playwright: 1.57.0 + dev: true + /@rollup/rollup-android-arm-eabi@4.14.1: resolution: {integrity: sha512-fH8/o8nSUek8ceQnT7K4EQbSiV7jgkHq81m9lWZFIXjJ7lJzpWXbQFpT/Zh6OZYnpFykvzC3fbEvEAFZu03dPA==} cpu: [arm] @@ -2040,6 +2066,12 @@ packages: tslib: 2.8.1 optional: true + /@types/cross-spawn@6.0.6: + resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} + dependencies: + '@types/node': 25.0.3 + dev: true + /@types/d3-scale-chromatic@3.0.3: resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==} dev: false @@ -2155,7 +2187,6 @@ packages: resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} dependencies: undici-types: 7.16.0 - dev: false /@types/normalize-package-data@2.4.4: resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -2840,8 +2871,8 @@ packages: which: 1.3.1 dev: true - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + /cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} dependencies: path-key: 3.1.1 @@ -3622,7 +3653,7 @@ packages: '@nodelib/fs.walk': 1.2.8 ajv: 6.12.6 chalk: 4.1.2 - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 debug: 4.3.4 escape-string-regexp: 4.0.0 eslint-scope: 8.0.1 @@ -3748,7 +3779,7 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 get-stream: 8.0.1 human-signals: 5.0.0 is-stream: 3.0.0 @@ -3918,7 +3949,7 @@ packages: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} engines: {node: '>=14'} dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 signal-exit: 4.1.0 dev: true @@ -3961,6 +3992,14 @@ packages: /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -4006,6 +4045,11 @@ packages: hasown: 2.0.2 dev: true + /get-port@7.1.0: + resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==} + engines: {node: '>=16'} + dev: true + /get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -6419,6 +6463,22 @@ packages: pathe: 1.1.2 dev: true + /playwright-core@1.57.0: + resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} + engines: {node: '>=18'} + hasBin: true + dev: true + + /playwright@1.57.0: + resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} + engines: {node: '>=18'} + hasBin: true + dependencies: + playwright-core: 1.57.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -7559,6 +7619,11 @@ packages: resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} dev: false + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + /trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} dev: false @@ -7703,7 +7768,6 @@ packages: /undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - dev: false /unhead@2.1.1: resolution: {integrity: sha512-NOt8n2KybAOxSLfNXegAVai4SGU8bPKqWnqCzNAvnRH2i8mW+0bbFjN/L75LBgCSTiOjJSpANe5w2V34Grr7Cw==} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..e266dbf --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['packages/**/*.test.ts'], + exclude: ['packages/**/*.spec.ts', 'node_modules/**/*'], + }, +});