Skip to content

Commit

Permalink
Add TypeScript annotations and documentation.
Browse files Browse the repository at this point in the history
- http() can no longer take a URL as first argument as a shorthand to
  make a GET request.
- runProcess() no longer returns a promise. Instead, the object it
  returns has a waitUntilExit() method

BREAKING CHANGE: http() no longer takes a string as shorthand.
runProcess() no longer returns a promise.
  • Loading branch information
vinsonchuong committed Nov 18, 2021
1 parent 31deddc commit 1d10a89
Show file tree
Hide file tree
Showing 12 changed files with 528 additions and 889 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/node_modules
/index.d.ts
15 changes: 5 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,9 @@ Returns an object with the following members:
- `outputStream`: A `Readable` stream for both stdout and stderr.
- `waitForOutput(pattern, timeout = 1000)`: Enables waiting for a given
substring or regular expression to be output, for up to a given timeout.
- `waitUntilExit()`: Returns a promise that will resolve with the exit code.
- `childProcess`: The underlying instance of `ChildProcess`

The returned object is also a `Promise` that when the process exits, resolves
with an object containing `output` and the exit `code`.

```js
import test from 'ava'
import {runProcess} from 'ava-patterns'
Expand Down Expand Up @@ -102,13 +100,11 @@ test('writing files', async (t) => {
```

### `http()`
Concisely send HTTP requests.

If given a URL string, sends a GET request and returns the response body.
Make an HTTP request.

Otherwise, takes in an object with properties `method`, `url`, `headers`, and an
optional `body` and returns an object with properties `status`, `headers`, and
`body`.
It takes in an object with properties `method`, `url`, optional `headers`, and
an optional `body` and returns an object with properties `status`, `headers`,
and `body`.

```js
import {http} from 'ava-patterns'
Expand All @@ -119,7 +115,6 @@ test('sending HTTP requests', async (t) => {
const response = await http({
method: 'POST',
url: 'https://httpbin.org/post',
headers: {},
body: 'Hello World!'
})
t.like(JSON.parse(response.body), {data: 'Hello World!'})
Expand Down
37 changes: 28 additions & 9 deletions http/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
import {sendRequest} from 'passing-notes'

export default async function (request) {
if (typeof request === 'string') {
const response = await sendRequest({
method: 'GET',
headers: {},
url: request,
})
return response.body
}
/**
* @typedef {'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'} Method
* @typedef {{ [name: string]: string }} Headers
* @typedef {string | Buffer} Data
* @typedef {Data | ReadableStream<Data> | AsyncIterable<Data>} Body
*
* @typedef {{
* method: Method,
* url: string,
* headers?: Headers,
* body?: Body
* }} Request
*
* @typedef {{
* status: number,
* headers: Headers,
* body: Body
* }} Response
*/

/**
* Make an HTTP request
*
* @param {Request} request - Request `method` and `url`, with optional
* `headers` and `body`
*
* @return {Promise<Response>} - The response `status`, `headers`, and `body`
*/
export default function (request) {
return sendRequest(request)
}
17 changes: 13 additions & 4 deletions http/index.test.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import test from 'ava'
import {http} from '../index.js'

test('sending a simple HTTP GET request', async (t) => {
t.regex(await http('http://example.com'), /Example Domain/)
})

test('sending an HTTP request', async (t) => {
const response = await http({
method: 'POST',
url: 'https://httpbin.org/post',
headers: {},
body: 'Hello World!',
})

if (typeof response.body !== 'string') {
return t.fail('Response body was not a string')
}

t.like(JSON.parse(response.body), {data: 'Hello World!'})
})

test('omitting unused fields', async (t) => {
const response = await http({
method: 'GET',
url: 'https://httpbin.org/status/200',
})
t.like(response, {status: 200})
})
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"repository": "vinsonchuong/ava-patterns",
"main": "index.js",
"scripts": {
"test": "xo && ava",
"test": "xo && tsc --noEmit && ava",
"prepublishOnly": "tsc --emitDeclarationOnly --outFile index.d.ts",
"release": "semantic-release"
},
"type": "module",
Expand All @@ -21,7 +22,8 @@
"tempy": "^2.0.0"
},
"devDependencies": {
"ava": "^3.15.0",
"@types/node": "^16.11.6",
"ava": "^4.0.0-rc.1",
"got": "^11.8.2",
"semantic-release": "^18.0.0",
"xo": "^0.45.0"
Expand Down
99 changes: 62 additions & 37 deletions run-process/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@ import {spawn} from 'node:child_process'
import {PassThrough} from 'node:stream'
import {wait} from '../index.js'

/**
* Create a temporary directory and delete it at the end of the test.
*
* @param {import('ava').ExecutionContext<any>} t - the AVA test context
* @param {Object} options
* @param {Array<string>} options.command - a shell command to spawn a process
* @param {{ [name: string]: string }} [options.env] - Environment variables to
* pass into the process
* @param {string} [options.cwd] - Working directory in which to run the process
*
* @return {{
* childProcess: import('node:child_process').ChildProcess
* output: string,
* outputStream: import('stream').Readable,
* waitForOutput(output: string | RegExp): Promise<void>,
* waitUntilExit(): Promise<number>
* }}
*/
export default function (
t,
{command: [command, ...args], env = {}, cwd = process.cwd()},
Expand All @@ -22,66 +40,73 @@ export default function (
}
})

const program = new Promise((resolve) => {
const exitCode = new Promise((resolve) => {
child.on('close', (code) => {
resolve({code, output: program.output})
resolve(code)
})
})
program.childProcess = child
program.output = ''
program.outputStream = new PassThrough()

program.outputStream.setEncoding('utf8')
let output = ''

const outputStream = new PassThrough()
outputStream.setEncoding('utf8')
child.stdout.setEncoding('utf8')
child.stderr.setEncoding('utf8')
Promise.all([
(async () => {
for await (const data of child.stdout) {
program.output += data
program.outputStream.write(data)
output += data
outputStream.write(data)
}
})(),
(async () => {
for await (const data of child.stderr) {
program.output += data
program.outputStream.write(data)
output += data
outputStream.write(data)
}
})(),
]).then(
() => {
program.outputStream.end()
outputStream.end()
},
(error) => {
program.outputStream.emit('error', error)
outputStream.emit('error', error)
},
)

program.waitForOutput = async (pattern, timeout = 1000) => {
const match =
typeof pattern === 'string'
? (string) => string.includes(pattern)
: (string) => Boolean(pattern.test(string))

await Promise.race([
(async () => {
await wait(timeout)
throw new Error(
`Timeout exceeded without seeing expected output:\n${program.output}`,
)
})(),
(async () => {
for await (const data of program.outputStream) {
if (match(data)) {
return
return {
childProcess: child,
get output() {
return output
},
outputStream,
async waitForOutput(pattern, timeout = 1000) {
await Promise.race([
(async () => {
await wait(timeout)
throw new Error(
`Timeout exceeded without seeing expected output:\n${output}`,
)
})(),
(async () => {
for await (const data of outputStream) {
if (
typeof pattern === 'string'
? data.includes(pattern)
: pattern.test(data)
) {
return
}
}
}

throw new Error(
`Process ended without emitting expected output:\n${program.output}`,
)
})(),
])
throw new Error(
`Process ended without emitting expected output:\n${output}`,
)
})(),
])
},
waitUntilExit() {
return exitCode
},
}

return program
}
7 changes: 3 additions & 4 deletions run-process/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,9 @@ test('setting the working directory and environment variables', async (t) => {
})

test('running a simple command that terminates', async (t) => {
const {output, code} = await runProcess(t, {
command: ['ls', '/'],
})
const program = runProcess(t, {command: ['ls', '/']})
const code = await program.waitUntilExit()

t.true(output.includes('tmp'))
t.true(program.output.includes('tmp'))
t.is(code, 0)
})
14 changes: 14 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"allowJs": true,
"checkJs": true,
"declaration": true
},
"exclude": [
"node_modules"
]
}
17 changes: 17 additions & 0 deletions use-temporary-directory/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@ import fs from 'fs-extra'
import tempy from 'tempy'
import stripIndent from 'strip-indent'

/**
* @typedef {{
* path: string,
* writeFile(filePath: string, fileContents: string): Promise<void>
* }} Directory
*/

/**
* Create a temporary directory and delete it at the end of the test.
*
* @param {import('ava').ExecutionContext<any>} t
*
* @return {Promise<{
* path: string,
* writeFile(filePath: string, fileContents: string): Promise<void>
* }>} an object allowing manipulation of files within the directory.
*/
export default async function (t) {
const directory = tempy.directory()

Expand Down
11 changes: 6 additions & 5 deletions use-temporary-directory/index.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import path from 'node:path'
import {promises as fs} from 'node:fs'
import {promisify} from 'node:util'
import childProcess from 'node:child_process'
import test from 'ava'
import {useTemporaryDirectory, runProcess} from '../index.js'
import {useTemporaryDirectory} from '../index.js'

const exec = promisify(childProcess.exec)

test.serial('creating a directory', async (t) => {
const directory = await useTemporaryDirectory(t)
Expand Down Expand Up @@ -39,10 +43,7 @@ test('automatically setting permissions for executable files', async (t) => {
`,
)

const {output} = await runProcess(t, {
command: ['./bin.js'],
cwd: directory.path,
})
const {stdout: output} = await exec('./bin.js', {cwd: directory.path})
t.is(output, 'Hello World!\n')
})

Expand Down
13 changes: 12 additions & 1 deletion wait/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
import {promisify} from 'node:util'

export default promisify(setTimeout)
const promisifiedSetTimeout = promisify(setTimeout)

/**
* Stop execution for a specified number of milliseconds
*
* @param {number} time
*
* @return {Promise<void>} Resolves after the specified amount of time
*/
export default async function (time) {
await promisifiedSetTimeout(time)
}
Loading

0 comments on commit 1d10a89

Please sign in to comment.