Skip to content

Commit

Permalink
chore: run tests in isolation (#332)
Browse files Browse the repository at this point in the history
  • Loading branch information
Bryce Kalow authored Jan 19, 2023
1 parent ae6c486 commit b3f026b
Show file tree
Hide file tree
Showing 23 changed files with 459 additions and 275 deletions.
1 change: 0 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,5 @@ jobs:
- name: Install and build
run: |
npm ci
npm run build
- name: Run Jest
run: npm run test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ __tests__/fixtures/*/out
examples/*/package-lock.json
examples/*/.next
dist/
*.tgz
172 changes: 169 additions & 3 deletions .jest/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,194 @@
import ReactDOMServer from 'react-dom/server'
import React from 'react'
import { VFileCompatible } from 'vfile'
import fs from 'fs'
import os from 'os'
import path from 'path'
import http from 'http'
import spawn from 'cross-spawn'
import { ChildProcess } from 'child_process'
import treeKill from 'tree-kill'
import puppeteer, { Browser } from 'puppeteer'
import { Server } from 'http'
import handler from 'serve-handler'

import { MDXRemote, MDXRemoteProps } from '../src/index'
import { serialize } from '../src/serialize'
import { SerializeOptions } from '../src/types'
import { VFileCompatible } from 'vfile'

export async function renderStatic(
mdx: VFileCompatible,
{
components,
scope,
mdxOptions,
minifyOptions,
parseFrontmatter,
}: SerializeOptions & Pick<MDXRemoteProps, 'components'> = {}
): Promise<string> {
const mdxSource = await serialize(mdx, {
mdxOptions,
minifyOptions,
parseFrontmatter,
})

return ReactDOMServer.renderToStaticMarkup(
<MDXRemote {...mdxSource} components={components} scope={scope} />
)
}

export async function getPathToPackedPackage() {
const packageJson = JSON.parse(
await fs.promises.readFile(
path.join(__dirname, '..', 'package.json'),
'utf-8'
)
)

const filename = `${packageJson.name}-${packageJson.version}.tgz`

return path.join(__dirname, '..', 'dist', filename)
}

// Create a temporary directory from one of our fixtures to run isolated tests in
// Handles installing the locally-packed next-mdx-remote
export async function createTmpTestDir(fixture) {
const tmpDir = await fs.promises.mkdtemp(
path.join(os.tmpdir(), `next-mdx-remote-${fixture}-`)
)

// copy over the fixture
const pathToFixture = path.join(
process.cwd(),
'__tests__',
'fixtures',
fixture
)

await fs.promises.cp(pathToFixture, tmpDir, { recursive: true })

// install locally packed package
const pathToPackedPackage = await getPathToPackedPackage()

console.log('installing dependencies in test directory')

spawn.sync('npm', ['install', pathToPackedPackage], {
cwd: tmpDir,
})

return tmpDir
}

async function cleanupTmpTestDir(tmpDir: string) {
await fs.promises.rm(tmpDir, { recursive: true, force: true })
}

// Handles creating an isolated test dir from one of the fixtures in __tests__/fixtures/
export function createDescribe(
name: string,
options: { fixture: string },
fn: ({ dir }: { dir: () => string; browser: () => Browser }) => void
): void {
describe(name, () => {
let tmpDir
let browser

beforeAll(async () => {
tmpDir = await createTmpTestDir(options.fixture)
browser = await puppeteer.launch()
})

fn({
dir() {
return tmpDir
},
browser() {
return browser
},
})

afterAll(async () => {
await browser.close()
await cleanupTmpTestDir(tmpDir)
})
})
}

// Starts a next dev server from the given directory on port 12333
export async function startDevServer(dir: string) {
const childProcess = spawn('npx', ['next', 'dev', '-p', '12333'], {
stdio: ['ignore', 'pipe', 'pipe'],
cwd: dir,
env: { ...process.env, NODE_ENV: 'development', __NEXT_TEST_MODE: 'true' },
})

childProcess.stderr?.on('data', (chunk) => {
process.stdout.write(chunk)
})

async function waitForStarted() {
return new Promise<undefined>((resolve) => {
childProcess.stdout?.on('data', (chunk) => {
const msg = chunk.toString()
process.stdout.write(chunk)

if (msg.includes('started server on') && msg.includes('url:')) {
resolve(undefined)
}
})
})
}

await waitForStarted()

return childProcess
}

// Stops running dev server using its ChildProcess instance
export async function stopDevServer(childProcess: ChildProcess) {
console.log('stopping development server...')
const promise = new Promise((resolve) => {
childProcess.on('close', () => {
console.log('development server stopped')
resolve(undefined)
})
})

await new Promise((resolve) => {
treeKill(childProcess.pid!, 'SIGKILL', () => resolve(undefined))
})

childProcess.kill('SIGKILL')

await promise
}

// Runs next build and next export in the provided directory
export function buildFixture(dir: string) {
spawn.sync('npx', ['next', 'build'], {
stdio: 'inherit',
cwd: dir,
env: { ...process.env, NODE_ENV: 'production', __NEXT_TEST_MODE: 'true' },
})
spawn.sync('npx', ['next', 'export'], {
stdio: 'inherit',
cwd: dir,
env: { ...process.env, NODE_ENV: 'production', __NEXT_TEST_MODE: 'true' },
})
}

// Helper to read an html file in the out directory relative to the provided dir
export function readOutputFile(dir: string, name: string) {
return fs.readFileSync(path.join(dir, 'out', `${name}.html`), 'utf8')
}

// Serves the out directory relative to the provided dir on port 1235
// TODO: we should just use next start
export function serveStatic(dir): Promise<Server> {
return new Promise((resolve) => {
const server = http.createServer((req, res) =>
handler(req, res, {
public: path.join(dir, 'out'),
})
)
server.listen(1235, () => resolve(server))
})
}
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ tsconfig.json
babel.config.js
src
.circleci
*.tgz
35 changes: 0 additions & 35 deletions __tests__/fixtures/basic/app/app-dir-mdx/provider.js

This file was deleted.

8 changes: 7 additions & 1 deletion __tests__/fixtures/basic/package.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
{
"private": true
"private": true,
"dependencies": {
"@hashicorp/remark-plugins": "^3.2.1",
"next": "^13.1.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
4 changes: 2 additions & 2 deletions __tests__/fixtures/basic/pages/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import fs from 'fs'
import path from 'path'
import { createContext, useEffect, useState } from 'react'
import dynamic from 'next/dynamic'
import { serialize } from '../../../../serialize'
import { MDXRemote } from '../../../../'
import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'
import Test from '../components/test'
import { paragraphCustomAlerts } from '@hashicorp/remark-plugins'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ import fs from 'fs'
import path from 'path'
import dynamic from 'next/dynamic'
import Test from '../../../components/test'
import { paragraphCustomAlerts } from '@hashicorp/remark-plugins'
import { Provider, Consumer } from '../provider'
import { compileMDX } from 'next-mdx-remote/rsc'

const MDX_COMPONENTS = {
Test,
ContextConsumer: Consumer,
strong: (props) => <strong className="custom-strong" {...props} />,
Dynamic: dynamic(() => import('../../../components/dynamic')),
}
Expand All @@ -21,15 +18,10 @@ export default async function Page() {
source,
components: MDX_COMPONENTS,
options: {
mdxOptions: { remarkPlugins: [paragraphCustomAlerts] },
mdxOptions: { remarkPlugins: [] },
parseFrontmatter: true,
},
})

return (
<>
<h1>{frontmatter.title}</h1>
<Provider>{content}</Provider>
</>
)
return <>{content}</>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ import fs from 'fs'
import path from 'path'
import dynamic from 'next/dynamic'
import Test from '../../../components/test'
import { paragraphCustomAlerts } from '@hashicorp/remark-plugins'
import { Provider, Consumer } from '../provider'
import { MDXRemote } from 'next-mdx-remote/rsc'

const MDX_COMPONENTS = {
Test,
ContextConsumer: Consumer,
strong: (props) => <strong className="custom-strong" {...props} />,
Dynamic: dynamic(() => import('../../../components/dynamic')),
}
Expand All @@ -19,15 +16,14 @@ export default async function Page() {

return (
<>
<Provider>
<MDXRemote
source={source}
components={MDX_COMPONENTS}
options={{
mdxOptions: { remarkPlugins: [paragraphCustomAlerts] },
}}
/>
</Provider>
<MDXRemote
source={source}
components={MDX_COMPONENTS}
options={{
mdxOptions: { remarkPlugins: [] },
parseFrontmatter: true,
}}
/>
</>
)
}
File renamed without changes.
File renamed without changes.
3 changes: 3 additions & 0 deletions __tests__/fixtures/rsc/components/dynamic.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Dynamic() {
return <div>I am a dynamic component.</div>
}
13 changes: 13 additions & 0 deletions __tests__/fixtures/rsc/components/test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use client'

import { useState } from 'react'

export default function Test({ name }) {
const [count, setCount] = useState(0)
return (
<>
<p>hello {name}</p>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
</>
)
}
32 changes: 32 additions & 0 deletions __tests__/fixtures/rsc/mdx/test.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
title: 'foo'
name: jeff
---

import Bar from 'bar'

# {frontmatter.title}

## Headline

<Test name={frontmatter.name} />

Some **markdown** content

<Dynamic />

```shell-session
curl localhost
```

This is more text.

"Authorize \<GITHUB_USER\>"

(support for version \<230)

### Some version \<= 1.3.x

#### metric.name.\<operation>.\<mount>

< 8ms
File renamed without changes.
Loading

0 comments on commit b3f026b

Please sign in to comment.