diff --git a/.all-contributorsrc b/.all-contributorsrc deleted file mode 100644 index 8bc3d0c..0000000 --- a/.all-contributorsrc +++ /dev/null @@ -1,64 +0,0 @@ -{ - "projectName": "cli-testing-library", - "projectOwner": "crutchcorn", - "repoType": "github", - "files": [ - "README.md" - ], - "imageSize": 100, - "commit": false, - "contributorsPerLine": 7, - "skipCi": false, - "contributors": [ - { - "login": "crutchcorn", - "name": "Corbin Crutchley", - "avatar_url": "https://avatars.githubusercontent.com/u/9100169?v=4", - "profile": "https://crutchcorn.dev/", - "contributions": [ - "code", - "doc", - "maintenance" - ] - }, - { - "login": "SerkanSipahi", - "name": "Bitcollage", - "avatar_url": "https://avatars.githubusercontent.com/u/1880749?v=4", - "profile": "https://www.linkedin.com/in/serkan-sipahi-59b20081/", - "contributions": [ - "doc" - ] - }, - { - "login": "jgoux", - "name": "Julien Goux", - "avatar_url": "https://avatars.githubusercontent.com/u/1443499?v=4", - "profile": "http://jgoux.dev", - "contributions": [ - "bug", - "code" - ] - }, - { - "login": "bantic", - "name": "Cory Forsyth", - "avatar_url": "https://avatars.githubusercontent.com/u/2023?v=4", - "profile": "http://coryforsyth.com/", - "contributions": [ - "doc" - ] - }, - { - "login": "eran-cohen", - "name": "eran-cohen", - "avatar_url": "https://avatars.githubusercontent.com/u/105227395?v=4", - "profile": "https://github.com/eran-cohen", - "contributions": [ - "doc" - ] - } - ], - "repoHost": "https://github.com", - "commitConvention": "none" -} diff --git a/.gitattributes b/.gitattributes index 6313b56..5a0d5e4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ +# Auto detect text files and perform LF normalization * text=auto eol=lf diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 0000000..a6179b0 --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,31 @@ +name: autofix.ci # needed to securely identify the workflow + +on: + pull_request: + push: + branches: [main, alpha, beta] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + autofix: + name: autofix + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4.2.2 + - name: Setup Tools + uses: tanstack/config/.github/setup@main + - name: Fix formatting + run: pnpm prettier:write + - name: Generate Docs + run: pnpm docs:generate + - name: Apply fixes + uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c + with: + commit-message: "ci: apply automated fixes and generate docs" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5b4bcd8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: ci + +on: + workflow_dispatch: + inputs: + tag: + description: override release tag + required: false + push: + branches: [main, alpha, beta] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: write + id-token: write + +jobs: + test-and-publish: + name: Test & Publish + if: github.repository == 'crutchcorn/cli-testing-library' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Tools + uses: tanstack/config/.github/setup@main + - name: Run Tests + run: pnpm run test:ci --parallel=3 + - name: Publish + run: | + git config --global user.name 'Corbin Crutchley' + git config --global user.email 'git@crutchcorn.dev' + npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}" + pnpm run cipublish + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + TAG: ${{ inputs.tag }} diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml new file mode 100644 index 0000000..fca47d3 --- /dev/null +++ b/.github/workflows/deploy-preview.yml @@ -0,0 +1,36 @@ +name: Deploy Preview + +on: + push: + branches: + - main + +permissions: {} + +jobs: + deploy-docs: + permissions: + contents: write # to write to gh-pages branch (peaceiris/actions-gh-pages) + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: "pnpm" + + - name: Install packages + run: pnpm install --frozen-lockfile --prefer-offline + + - name: Build + run: pnpm run build:website + + - name: Deploy docs + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: website/dist + cname: cli-testing.com diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..f2bd439 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,41 @@ +name: pr + +on: + pull_request: + paths-ignore: + - "docs/**" + - "media/**" + - "**/*.md" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Tools + uses: tanstack/config/.github/setup@main + - name: Run Checks + run: pnpm run test:pr --parallel=3 + preview: + name: Preview + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup Tools + uses: tanstack/config/.github/setup@main + - name: Build Packages + run: pnpm run build:all + - name: Publish Previews + run: pnpx pkg-pr-new publish --pnpm --compact './packages/*' --template './examples/*/*' diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml deleted file mode 100644 index d35ef34..0000000 --- a/.github/workflows/validate.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: validate -on: - push: - branches: - - '+([0-9])?(.{+([0-9]),x}).x' - - 'main' - - 'next' - - 'next-major' - - 'beta' - - 'alpha' - - '!all-contributors/**' - pull_request: {} -jobs: - main: - # ignore all-contributors PRs - if: ${{ !contains(github.head_ref, 'all-contributors') }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest] - node: [12, 14, 16] - runs-on: ${{ matrix.os }} - steps: - - name: 🛑 Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.9.0 - - - name: ⬇️ Checkout repo - uses: actions/checkout@v2 - - - name: ⎔ Setup node - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node }} - - - name: 📥 Download deps - uses: bahmutov/npm-install@v1 - with: - useLockFile: false - - - name: ▶️ Run validate script - run: npm run validate - - - name: ⬆️ Upload coverage report - uses: codecov/codecov-action@v2 - - release: - needs: main - runs-on: ubuntu-latest - if: - ${{ github.repository == 'crutchcorn/cli-testing-library' && - contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha', - github.ref) && github.event_name == 'push' }} - steps: - - name: 🛑 Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.9.0 - - - name: ⬇️ Checkout repo - uses: actions/checkout@v2 - - - name: ⎔ Setup node - uses: actions/setup-node@v2 - with: - node-version: 14 - - - name: 📥 Download deps - uses: bahmutov/npm-install@v1 - with: - useLockFile: false - - - name: 🏗 Run build script - run: npm run build - - - name: 🚀 Release - uses: cycjimmy/semantic-release-action@v2 - with: - semantic_version: 17 - branches: | - [ - '+([0-9])?(.{+([0-9]),x}).x', - 'main', - 'next', - 'next-major', - {name: 'beta', prerelease: true}, - {name: 'alpha', prerelease: true} - ] - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index cf1a543..d60197e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,42 @@ + +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies node_modules +package-lock.json +yarn.lock + +# builds +build coverage dist + +# misc .DS_Store -.idea/ +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +.next -# these cause more harm than good -# when working with contributors -package-lock.json -yarn.lock -# Temporary for dev -old/ -reference/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.history +size-plugin.json +stats-hydration.json +stats.json +stats.html +.vscode/settings.json + +*.log +.cache +.idea +.nx/cache +.nx/workspace-data +.pnpm-store +.tsup + +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/.huskyrc.js b/.huskyrc.js deleted file mode 100644 index 5e45c45..0000000 --- a/.huskyrc.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('kcd-scripts/husky') diff --git a/.npmrc b/.npmrc index 1df2a6d..84aee8d 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,3 @@ -registry=https://registry.npmjs.org/ -package-lock=false +link-workspace-packages=true +prefer-workspace-packages=true +provenance=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..1d9b783 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.12.0 diff --git a/.prettierignore b/.prettierignore index 9c62828..6534da9 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,9 @@ -node_modules -coverage -dist +**/.next +**/.nx/cache +**/.svelte-kit +**/build +**/coverage +**/dist +**/docs +**/codemods/**/__testfixtures__ +pnpm-lock.yaml diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index 4679d9b..0000000 --- a/.prettierrc.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('kcd-scripts/prettier') diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index ba61187..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,128 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, religion, or sexual identity and -orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -- Demonstrating empathy and kindness toward other people -- Being respectful of differing opinions, viewpoints, and experiences -- Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -- Focusing on what is best not just for us as individuals, but for the overall - community - -Examples of unacceptable behavior include: - -- The use of sexualized language or imagery, and sexual attention or advances of - any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or email address, - without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -testingcli+coc@oceanbit.dev. All complaints will be reviewed and investigated -promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series of -actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or permanent -ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within the -community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. - -Community Impact Guidelines were inspired by -[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 368af30..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,37 +0,0 @@ -# Contributing - -Thanks for being willing to contribute! - -## Project setup - -1. Fork and clone the repo -2. Run `npm run setup -s` to install dependencies and run validation -3. Create a branch for your PR with `git checkout -b pr/your-branch-name` - -> Tip: Keep your `main` branch pointing at the original repository and make pull -> requests from branches on your fork. To do this, run: -> -> ``` -> git remote add upstream https://github.com/crutchcorn/cli-testing-library.git -> git fetch upstream -> git branch --set-upstream-to=upstream/main main -> ``` -> -> This will add the original repository as a "remote" called "upstream," Then -> fetch the git information from that remote, then set your local `main` branch -> to use the upstream main branch whenever you run `git pull`. Then you can make -> all of your pull request branches based on this `main` branch. Whenever you -> want to update your version of `main`, do a regular `git pull`. - -## Committing and Pushing changes - -Please make sure to run the tests before you commit your changes. You can run -`npm run test:update` which will update any snapshots that need updating. Make -sure to include those changes (if they exist) in your commit. - -## Help needed - -Please checkout the [the open issues][issues] - -Also, please watch the repo and respond to questions/bug reports/feature -requests! Thanks! diff --git a/README.md b/README.md deleted file mode 100644 index 94427f1..0000000 --- a/README.md +++ /dev/null @@ -1,137 +0,0 @@ -
-

CLI Testing Library

- - - koala - - -

Simple and complete CLI testing utilities that encourage good testing -practices.

- -
- -
- -[![Build Status](https://img.shields.io/github/actions/workflow/status/crutchcorn/cli-testing-library/validate.yml?branch=main&style=flat-square)](https://github.com/crutchcorn/cli-testing-library/actions/workflows/validate.yml?query=branch%3Amain) -[![version](https://img.shields.io/npm/v/cli-testing-library?style=flat-square)](https://www.npmjs.com/package/cli-testing-library) -[![downloads](https://img.shields.io/npm/dw/cli-testing-library?style=flat-square)](https://www.npmjs.com/package/cli-testing-library) -[![MIT License](https://img.shields.io/npm/l/cli-testing-library?style=flat-square)](./LICENSE) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](makeapullrequest.com) -[![Code of Conduct](https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square)](./CODE_OF_CONDUCT.md) - - - -[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-) - - - -[![Watch on GitHub](https://img.shields.io/github/watchers/crutchcorn/cli-testing-library.svg?style=social)](https://github.com/crutchcorn/cli-testing-library/watchers) -[![Star on GitHub](https://img.shields.io/github/stars/crutchcorn/cli-testing-library.svg?style=social)](https://github.com/crutchcorn/cli-testing-library/stargazers) - - - -## Table of Contents - - - - -- [Installation](#installation) -- [Usage](#usage) -- [Contributors ✨](#contributors-) - - - -> This project is not affiliated with the -> ["Testing Library"](https://github.com/testing-library) ecosystem that this -> project is clearly inspired from. We're just big fans :) - -## Installation - -This module is distributed via [npm][npm] which is bundled with [node][node] and -should be installed as one of your project's `devDependencies`: - -``` -npm install --save-dev cli-testing-library -``` - -## Usage - -> This is currently the only section of "usage" documentation. We'll be -> expanding it as soon as possible - -Usage example: - -```javascript -const {resolve} = require('path') -const {render} = require('cli-testing-library') - -test('Is able to make terminal input and view in-progress stdout', async () => { - const {clear, findByText, queryByText, userEvent} = await render('node', [ - resolve(__dirname, './execute-scripts/stdio-inquirer.js'), - ]) - - const instance = await findByText('First option') - - expect(instance).toBeInTheConsole() - - expect(await findByText('❯ One')).toBeInTheConsole() - - clear() - - userEvent('[ArrowDown]') - - expect(await findByText('❯ Two')).toBeInTheConsole() - - clear() - - userEvent.keyboard('[Enter]') - - expect(await findByText('First option: Two')).toBeInTheConsole() - expect(await queryByText('First option: Three')).not.toBeInTheConsole() -}) -``` - -For a API reference documentation, including suggestions on how to use this -library, see our -[documentation introduction with further reading](./docs/introduction.md). - -> While this library _does_ work in Windows, it does not appear to function -> properly in Windows CI environments, such as GitHub actions. As a result, you -> may need to either switch CI systems or limit your CI to only run in Linux -> -> If you know how to fix this, please let us know in -> [this tracking issue](https://github.com/crutchcorn/cli-testing-library/issues/3) - -## Contributors ✨ - -Thanks goes to these wonderful people -([emoji key](https://allcontributors.org/docs/en/emoji-key)): - - - - - - - - - - - - - - -
Corbin Crutchley
Corbin Crutchley

💻 📖 🚧
Bitcollage
Bitcollage

📖
Julien Goux
Julien Goux

🐛 💻
Cory Forsyth
Cory Forsyth

📖
eran-cohen
eran-cohen

📖
- - - - - - -This project follows the -[all-contributors](https://github.com/all-contributors/all-contributors) -specification. Contributions of any kind welcome! diff --git a/docs/api.md b/docs/api.md index b2ce96e..550a96f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,4 +1,6 @@ -# API +--- +title: "API" +--- `CLI Testing Library`, despite taking clear inspiration from, does not re-export anything from @@ -14,25 +16,6 @@ them. Instead, the following API is what `CLI Testing Library` provides the following. - - - -- [`render`](#render) -- [`render` Options](#render-options) - - [`cwd`](#cwd) - - [`spawnOpts`](#spawnopts) -- [`render` Result](#render-result) - - [`...queries`](#queries) - - [ByText](#bytext) - - [`userEvent[eventName]`](#usereventeventname) - - [`debug`](#debug) - - [`hasExit`](#hasexit) - - [`process`](#process) - - [`stdoutArr`/`stderrArr`](#stdoutarrstderrarr) - - [`clear`](#clear) - - - # `render` ```typescript @@ -113,7 +96,7 @@ The most important feature of render is that the queries from [CLI Testing Library](https://github.com/crutchcorn/cli-testing-library) are automatically returned with their first argument bound to the testInstance. -See [Queries](./queries.md) to learn more about how to use these queries and the +See [Queries](./queries) to learn more about how to use these queries and the philosophy behind them. ### ByText @@ -139,7 +122,7 @@ Queries for test instance `stdout` results with the given text (and it also accepts a TextMatch). These options are all standard for text matching. To learn more, see our -[Queries page](./queries.md). +[Queries page](./queries). ## `userEvent[eventName]` @@ -149,11 +132,11 @@ userEvent[eventName](...eventProps) > While `userEvent` isn't usually returned on `render` in, say, > `React Testing Library`, we're able to do so because of our differences in -> implementation with upstream. See our [Differences](./differences.md) doc for +> implementation with upstream. See our [Differences](./differences) doc for > more. This object is the same as described with -[`userEvent` documentation](./user-event.md) with the key difference that +[`userEvent` documentation](./user-event) with the key difference that `instance` is not expected to be passed when bound to `render`. ## `debug` @@ -175,7 +158,7 @@ debug() ``` This is a simple wrapper around `prettyCLI` which is also exposed and comes from -[CLI Testing Library](./debug.md). +[CLI Testing Library](./debug). ## `hasExit` @@ -201,7 +184,7 @@ so you can call `process.pid` etc. to inspect the process. ## `stdoutArr`/`stderrArr` Each of these is an array of what's output by their respective `std`\* pipe. -This is used internally to create the [`debug`methods](./debug.md) and more. +This is used internally to create the [`debug`methods](./debug) and more. They're defined as: ```typescript @@ -224,7 +207,7 @@ end of the test. > Please note that this is done automatically if the testing framework you're > using supports the `afterEach` global and it is injected to your testing -> environment (like mocha, Jest, and Jasmine). If not, you will need to do +> environment (like mocha, Jest, Vitest, and Jasmine). If not, you will need to do > manual cleanups after each test. For example, if you're using the [ava](https://github.com/avajs/ava) testing diff --git a/docs/configure.md b/docs/configure.md index ca80f4a..b798212 100644 --- a/docs/configure.md +++ b/docs/configure.md @@ -1,17 +1,6 @@ -# Configuration Options - - - - -- [Introduction](#introduction) -- [Options](#options) - - [`showOriginalStackTrace`](#showoriginalstacktrace) - - [`throwSuggestions` (experimental)](#throwsuggestions-experimental) - - [`getInstanceError`](#getinstanceerror) - - [`asyncUtilTimeout`](#asyncutiltimeout) - - [`renderAwaitTime`](#renderawaittime) - - +--- +title: "Configuration Options" +--- ## Introduction @@ -36,7 +25,7 @@ to `waitFor`. ### `throwSuggestions` (experimental) -When enabled, if [better queries](./queries.md) are available the test will fail +When enabled, if [better queries](./queries) are available the test will fail and provide a suggested query to use instead. Default to `false`. To disable a suggestion for a single query just add `{suggest:false}` as an @@ -62,7 +51,7 @@ to 1000ms. By default, we wait for the CLI to `spawn` the command from `render`. If we immediately resolve the promise to allow users to query, however, we lose the ability to `getByText` immediately after rendering. This -[differs greatly from upstream Testing Library](./differences.md) and makes for +[differs greatly from upstream Testing Library](./differences) and makes for a poor testing experience. As a result, we wait this duration before resolving the promise after the diff --git a/docs/debug.md b/docs/debug.md index 1cf9a78..93184a1 100644 --- a/docs/debug.md +++ b/docs/debug.md @@ -1,12 +1,6 @@ -# Debug - - - - -- [Automatic Logging](#automatic-logging) -- [`prettyCLI`](#prettycli) - - +--- +title: "Debug" +--- ## Automatic Logging diff --git a/docs/differences.md b/docs/differences.md index ce3576f..4800dc8 100644 --- a/docs/differences.md +++ b/docs/differences.md @@ -1,4 +1,6 @@ -# Differences Between `cli-testing-library` & Testing Library +--- +title: "Differences Between CLI Testing Library & Testing Library" +--- While we clearly take inspiration from [`DOM Testing Library`](https://github.com/testing-library/dom-testing-library) @@ -7,19 +9,6 @@ and and try to do our best to align as much as possible, there are some major distinctions between the project's APIs. - - - -- [Instances](#instances) -- [Queries](#queries) -- [Events](#events) - - [FireEvent](#fireevent) - - [UserEvent](#userevent) -- [Matchers](#matchers) -- [Similarities](#similarities) - - - # Instances While the `DOM Testing Library` and it's descendants have a concept of both a diff --git a/docs/fire-event.md b/docs/fire-event.md index 2456a64..edac334 100644 --- a/docs/fire-event.md +++ b/docs/fire-event.md @@ -1,13 +1,6 @@ - - - -- [Firing Events](#firing-events) - - [`fireEvent`](#fireevent) - - [`fireEvent[eventName]`](#fireeventeventname) - - - -# Firing Events +--- +title: "Firing Events" +--- > **Note** > diff --git a/docs/introduction.md b/docs/introduction.md index 19c39dc..a876055 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -1,4 +1,6 @@ -# CLI Testing Library +--- +title: "Introduction" +--- [CLI Testing Library](https://github.com/crutchcorn/cli-testing-library) implements as-close-as-possible API to @@ -13,16 +15,6 @@ npm install --save-dev cli-testing-library - [`cli-testing-library` on GitHub](https://github.com/crutchcorn/cli-testing-library) -## Table of Contents - - - - -- [The problem](#the-problem) -- [This solution](#this-solution) - - - # The problem You want to write maintainable tests for your CLI applications. As a part of @@ -55,16 +47,15 @@ will work when a real user uses it. **What this library is not**: 1. A test runner or framework -2. Specific to a testing framework (though we recommend Jest as our preference, - the library works with any framework.) +2. Specific to a testing framework # Further Reading -- [API reference for `render` and friends](./api.md) -- [Jest matchers](./matchers.md) -- [Mock user input](./user-event.md) -- [Manually fire input events](./fire-event.md) -- [Output matching queries](./queries.md) -- [Debugging "CLI Testing Library" tests](./debug.md) -- [Configure library options](./configure.md) -- [Differences between us and other "Testing Library" projects](./differences.md) +- [API reference for `render` and friends](./api) +- [Jest and Vitest matchers](./matchers) +- [Mock user input](./user-event) +- [Manually fire input events](./fire-event) +- [Output matching queries](./queries) +- [Debugging "CLI Testing Library" tests](./debug) +- [Configure library options](./configure) +- [Differences between us and other "Testing Library" projects](./differences) diff --git a/docs/matchers.md b/docs/matchers.md index 83a0dca..b252736 100644 --- a/docs/matchers.md +++ b/docs/matchers.md @@ -1,33 +1,20 @@ -# Matchers +--- +title: "Matchers" +--- -The `cli-testing-library` provides a set of custom jest matchers that you can -use to extend jest. These will make your tests more declarative, clear to read +The `cli-testing-library` provides a set of custom Jest and Vitest matchers that you can +use to extend Jest or Vitest. These will make your tests more declarative, clear to read and to maintain. -## Table of Contents - - - - -- [Usage](#usage) - - [With TypeScript](#with-typescript) -- [Custom matchers](#custom-matchers) - - [`toBeInTheConsole`](#tobeintheconsole) - - [Examples](#examples) - - [`toHaveErrorMessage`](#tohaveerrormessage) - - [Examples](#examples-1) - - - ## Usage -Import `cli-testing-library/extend-expect` once (for instance in your +Import `cli-testing-library/jest`, `cli-testing-library/jest-globals`, or `cli-testing-library/vitest` once, based on your testing (for instance in your [tests setup file](https://jestjs.io/docs/en/configuration.html#setupfilesafterenv-array)) and you're good to go: ```javascript // In your own jest-setup.js (or any other name) -import 'cli-testing-library/extend-expect' +import 'cli-testing-library/jest' // In jest.config.js add (if you haven't already) setupFilesAfterEnv: ['/jest-setup.js'] diff --git a/docs/queries.md b/docs/queries.md index 6fad12a..e9a7a6b 100644 --- a/docs/queries.md +++ b/docs/queries.md @@ -1,4 +1,6 @@ -# About Queries +--- +title: "About Queries" +--- Queries are the methods that the CLI Testing Library gives you to find processes in the command line. There are several types of queries ("get", "find", @@ -7,29 +9,10 @@ if no CLI instance is found or if it will return a Promise and retry. Depending on what app content you are selecting, different queries may be more or less appropriate. -While our APIs [differ slightly](./differences.md) from upstream Testing +While our APIs [differ slightly](./differences) from upstream Testing Library's, [their "About Queries" page summarizes the goals and intentions of this project's queries quite well](https://testing-library.com/docs/queries/about/). - - - -- [Example](#example) -- [Types of Queries](#types-of-queries) -- [`TextMatch`](#textmatch) - - [TextMatch Examples](#textmatch-examples) - - [Precision](#precision) - - [Normalization](#normalization) - - [Normalization Examples](#normalization-examples) -- [ByText](#bytext) - - [API](#api) - - [Options](#options) -- [ByError](#byerror) - - [API](#api-1) - - [Options](#options-1) - - - # Example ```javascript diff --git a/docs/reference/classes/mutationobserver.md b/docs/reference/classes/mutationobserver.md new file mode 100644 index 0000000..159241e --- /dev/null +++ b/docs/reference/classes/mutationobserver.md @@ -0,0 +1,82 @@ +--- +id: MutationObserver +title: MutationObserver +--- + + + +# Class: MutationObserver + +Defined in: [mutation-observer.ts:9](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/mutation-observer.ts#L9) + +## Constructors + +### new MutationObserver() + +```ts +new MutationObserver(cb): MutationObserver +``` + +Defined in: [mutation-observer.ts:13](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/mutation-observer.ts#L13) + +#### Parameters + +##### cb + +() => `void` + +#### Returns + +[`MutationObserver`](mutationobserver.md) + +## Properties + +### \_cb() + +```ts +_cb: () => void; +``` + +Defined in: [mutation-observer.ts:10](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/mutation-observer.ts#L10) + +#### Returns + +`void` + +*** + +### \_id + +```ts +_id: number; +``` + +Defined in: [mutation-observer.ts:11](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/mutation-observer.ts#L11) + +## Methods + +### disconnect() + +```ts +disconnect(): void +``` + +Defined in: [mutation-observer.ts:22](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/mutation-observer.ts#L22) + +#### Returns + +`void` + +*** + +### observe() + +```ts +observe(): void +``` + +Defined in: [mutation-observer.ts:18](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/mutation-observer.ts#L18) + +#### Returns + +`void` diff --git a/docs/reference/functions/bindobjectfnstoinstance.md b/docs/reference/functions/bindobjectfnstoinstance.md new file mode 100644 index 0000000..e75d7ed --- /dev/null +++ b/docs/reference/functions/bindobjectfnstoinstance.md @@ -0,0 +1,31 @@ +--- +id: bindObjectFnsToInstance +title: bindObjectFnsToInstance +--- + + + +# Function: bindObjectFnsToInstance() + +```ts +function bindObjectFnsToInstance(instance, object): Record unknown> +``` + +Defined in: [helpers.ts:75](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/helpers.ts#L75) + +This is used to bind a series of functions where `instance` is the first argument +to an instance, removing the implicit first argument. + +## Parameters + +### instance + +`TestInstance` + +### object + +`Record`\<`string`, (...`props`) => `unknown`\> + +## Returns + +`Record`\<`string`, (...`props`) => `unknown`\> diff --git a/docs/reference/functions/buildqueries.md b/docs/reference/functions/buildqueries.md new file mode 100644 index 0000000..d8dd6b0 --- /dev/null +++ b/docs/reference/functions/buildqueries.md @@ -0,0 +1,28 @@ +--- +id: buildQueries +title: buildQueries +--- + + + +# Function: buildQueries() + +```ts +function buildQueries(queryBy, getMissingError): readonly [(container, ...args) => T, (container, ...args) => T, (instance, text, options?, waitForOptions?) => Promise] +``` + +Defined in: [query-helpers.ts:115](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/query-helpers.ts#L115) + +## Parameters + +### queryBy + +[`QueryMethod`](../type-aliases/querymethod.md)\<\[[`Matcher`](../type-aliases/matcher.md), [`MatcherOptions`](../interfaces/matcheroptions.md)\], `null` \| `TestInstance`\> + +### getMissingError + +[`GetErrorFunction`](../type-aliases/geterrorfunction.md)\<\[[`Matcher`](../type-aliases/matcher.md), [`MatcherOptions`](../interfaces/matcheroptions.md)\]\> + +## Returns + +readonly \[\<`T`\>(`container`, ...`args`) => `T`, \<`T`\>(`container`, ...`args`) => `T`, \<`T`\>(`instance`, `text`, `options`?, `waitForOptions`?) => `Promise`\<`T`\>\] diff --git a/docs/reference/functions/cleanup.md b/docs/reference/functions/cleanup.md new file mode 100644 index 0000000..1c25f79 --- /dev/null +++ b/docs/reference/functions/cleanup.md @@ -0,0 +1,18 @@ +--- +id: cleanup +title: cleanup +--- + + + +# Function: cleanup() + +```ts +function cleanup(): Promise +``` + +Defined in: [pure.ts:167](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/pure.ts#L167) + +## Returns + +`Promise`\<`void`[]\> diff --git a/docs/reference/functions/configure.md b/docs/reference/functions/configure.md new file mode 100644 index 0000000..e71d1ca --- /dev/null +++ b/docs/reference/functions/configure.md @@ -0,0 +1,24 @@ +--- +id: configure +title: configure +--- + + + +# Function: configure() + +```ts +function configure(newConfig): void +``` + +Defined in: [config.ts:77](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/config.ts#L77) + +## Parameters + +### newConfig + +[`ConfigFn`](../interfaces/configfn.md) | `Partial`\<[`Config`](../interfaces/config.md)\> + +## Returns + +`void` diff --git a/docs/reference/functions/debounce.md b/docs/reference/functions/debounce.md new file mode 100644 index 0000000..7a0944a --- /dev/null +++ b/docs/reference/functions/debounce.md @@ -0,0 +1,42 @@ +--- +id: debounce +title: debounce +--- + + + +# Function: debounce() + +```ts +function debounce(func, timeout): (...args) => void +``` + +Defined in: [helpers.ts:56](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/helpers.ts#L56) + +## Type Parameters + +• **T** *extends* (...`args`) => `void` + +## Parameters + +### func + +`T` + +### timeout + +`number` + +## Returns + +`Function` + +### Parameters + +#### args + +...`Parameters`\<`T`\> + +### Returns + +`void` diff --git a/docs/reference/functions/fireevent.md b/docs/reference/functions/fireevent.md new file mode 100644 index 0000000..e388ea8 --- /dev/null +++ b/docs/reference/functions/fireevent.md @@ -0,0 +1,39 @@ +--- +id: fireEvent +title: fireEvent +--- + + + +# Function: fireEvent() + +```ts +function fireEvent( + instance, + event, +options?): boolean | Promise +``` + +Defined in: [events.ts:20](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/events.ts#L20) + +## Type Parameters + +• **TEventType** *extends* `"sigterm"` \| `"sigkill"` \| `"write"` + +## Parameters + +### instance + +`TestInstance` + +### event + +`TEventType` + +### options? + +`Parameters`\<`object`\[`TEventType`\]\>\[`1`\] + +## Returns + +`boolean` \| `Promise`\<`void`\> diff --git a/docs/reference/functions/getconfig.md b/docs/reference/functions/getconfig.md new file mode 100644 index 0000000..05918ee --- /dev/null +++ b/docs/reference/functions/getconfig.md @@ -0,0 +1,18 @@ +--- +id: getConfig +title: getConfig +--- + + + +# Function: getConfig() + +```ts +function getConfig(): Config +``` + +Defined in: [config.ts:91](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/config.ts#L91) + +## Returns + +[`Config`](../interfaces/config.md) diff --git a/docs/reference/functions/getcurrentinstance.md b/docs/reference/functions/getcurrentinstance.md new file mode 100644 index 0000000..33f838c --- /dev/null +++ b/docs/reference/functions/getcurrentinstance.md @@ -0,0 +1,18 @@ +--- +id: getCurrentInstance +title: getCurrentInstance +--- + + + +# Function: getCurrentInstance() + +```ts +function getCurrentInstance(): undefined | TestInstance +``` + +Defined in: [helpers.ts:33](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/helpers.ts#L33) + +## Returns + +`undefined` \| `TestInstance` diff --git a/docs/reference/functions/getdefaultnormalizer.md b/docs/reference/functions/getdefaultnormalizer.md new file mode 100644 index 0000000..5c449c9 --- /dev/null +++ b/docs/reference/functions/getdefaultnormalizer.md @@ -0,0 +1,24 @@ +--- +id: getDefaultNormalizer +title: getDefaultNormalizer +--- + + + +# Function: getDefaultNormalizer() + +```ts +function getDefaultNormalizer(__namedParameters): NormalizerFn +``` + +Defined in: [matches.ts:104](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L104) + +## Parameters + +### \_\_namedParameters + +[`DefaultNormalizerOptions`](../interfaces/defaultnormalizeroptions.md) = `{}` + +## Returns + +[`NormalizerFn`](../type-aliases/normalizerfn.md) diff --git a/docs/reference/functions/getinstanceerror.md b/docs/reference/functions/getinstanceerror.md new file mode 100644 index 0000000..9fcad19 --- /dev/null +++ b/docs/reference/functions/getinstanceerror.md @@ -0,0 +1,28 @@ +--- +id: getInstanceError +title: getInstanceError +--- + + + +# Function: getInstanceError() + +```ts +function getInstanceError(message, instance): Error +``` + +Defined in: [query-helpers.ts:26](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/query-helpers.ts#L26) + +## Parameters + +### message + +`null` | `string` + +### instance + +`TestInstance` + +## Returns + +`Error` diff --git a/docs/reference/functions/getqueriesforelement.md b/docs/reference/functions/getqueriesforelement.md new file mode 100644 index 0000000..8076214 --- /dev/null +++ b/docs/reference/functions/getqueriesforelement.md @@ -0,0 +1,43 @@ +--- +id: getQueriesForElement +title: getQueriesForElement +--- + + + +# Function: getQueriesForElement() + +```ts +function getQueriesForElement( + instance, + queries, +initialValue): BoundFunctions +``` + +Defined in: [get-queries-for-instance.ts:50](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/get-queries-for-instance.ts#L50) + +## Type Parameters + +• **T** *extends* [`Queries`](../interfaces/queries.md) = [`queries`](../namespaces/queries/index.md) + +## Parameters + +### instance + +`TestInstance` + +### queries + +`T` = `...` + +object of functions + +### initialValue + +for reducer + +## Returns + +[`BoundFunctions`](../type-aliases/boundfunctions.md)\<`T`\> + +returns object of functions bound to container diff --git a/docs/reference/functions/jestfaketimersareenabled.md b/docs/reference/functions/jestfaketimersareenabled.md new file mode 100644 index 0000000..eb29455 --- /dev/null +++ b/docs/reference/functions/jestfaketimersareenabled.md @@ -0,0 +1,18 @@ +--- +id: jestFakeTimersAreEnabled +title: jestFakeTimersAreEnabled +--- + + + +# Function: jestFakeTimersAreEnabled() + +```ts +function jestFakeTimersAreEnabled(): boolean +``` + +Defined in: [helpers.ts:3](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/helpers.ts#L3) + +## Returns + +`boolean` diff --git a/docs/reference/functions/makefindquery.md b/docs/reference/functions/makefindquery.md new file mode 100644 index 0000000..25a0bfc --- /dev/null +++ b/docs/reference/functions/makefindquery.md @@ -0,0 +1,54 @@ +--- +id: makeFindQuery +title: makeFindQuery +--- + + + +# Function: makeFindQuery() + +```ts +function makeFindQuery(getter): (instance, text, options?, waitForOptions?) => Promise +``` + +Defined in: [query-helpers.ts:66](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/query-helpers.ts#L66) + +## Type Parameters + +• **TQueryFor** + +## Parameters + +### getter + +(`container`, `text`, `options`?) => `TQueryFor` + +## Returns + +`Function` + +### Type Parameters + +• **T** *extends* `TestInstance` = `TestInstance` + +### Parameters + +#### instance + +`TestInstance` + +#### text + +[`Matcher`](../type-aliases/matcher.md) + +#### options? + +[`MatcherOptions`](../interfaces/matcheroptions.md) + +#### waitForOptions? + +[`waitForOptions`](../interfaces/waitforoptions.md) + +### Returns + +`Promise`\<`T`\> diff --git a/docs/reference/functions/makenormalizer.md b/docs/reference/functions/makenormalizer.md new file mode 100644 index 0000000..7193bed --- /dev/null +++ b/docs/reference/functions/makenormalizer.md @@ -0,0 +1,28 @@ +--- +id: makeNormalizer +title: makeNormalizer +--- + + + +# Function: makeNormalizer() + +```ts +function makeNormalizer(props): NormalizerFn +``` + +Defined in: [matches.ts:132](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L132) + +## Parameters + +### props + +[`NormalizerOptions`](../interfaces/normalizeroptions.md) + +Constructs a normalizer to pass to functions in matches.js + +## Returns + +[`NormalizerFn`](../type-aliases/normalizerfn.md) + +A normalizer diff --git a/docs/reference/functions/render.md b/docs/reference/functions/render.md new file mode 100644 index 0000000..6f79208 --- /dev/null +++ b/docs/reference/functions/render.md @@ -0,0 +1,35 @@ +--- +id: render +title: render +--- + + + +# Function: render() + +```ts +function render( + command, + args, +opts): Promise +``` + +Defined in: [pure.ts:40](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/pure.ts#L40) + +## Parameters + +### command + +`string` + +### args + +`string`[] = `[]` + +### opts + +`Partial`\<[`RenderOptions`](../interfaces/renderoptions.md)\> = `{}` + +## Returns + +`Promise`\<[`RenderResult`](../type-aliases/renderresult.md)\> diff --git a/docs/reference/functions/runobservers.md b/docs/reference/functions/runobservers.md new file mode 100644 index 0000000..0baa3a9 --- /dev/null +++ b/docs/reference/functions/runobservers.md @@ -0,0 +1,18 @@ +--- +id: _runObservers +title: _runObservers +--- + + + +# Function: \_runObservers() + +```ts +function _runObservers(): void +``` + +Defined in: [mutation-observer.ts:27](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/mutation-observer.ts#L27) + +## Returns + +`void` diff --git a/docs/reference/functions/runwithexpensiveerrordiagnosticsdisabled.md b/docs/reference/functions/runwithexpensiveerrordiagnosticsdisabled.md new file mode 100644 index 0000000..98d070f --- /dev/null +++ b/docs/reference/functions/runwithexpensiveerrordiagnosticsdisabled.md @@ -0,0 +1,28 @@ +--- +id: runWithExpensiveErrorDiagnosticsDisabled +title: runWithExpensiveErrorDiagnosticsDisabled +--- + + + +# Function: runWithExpensiveErrorDiagnosticsDisabled() + +```ts +function runWithExpensiveErrorDiagnosticsDisabled(callback): T +``` + +Defined in: [config.ts:66](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/config.ts#L66) + +## Type Parameters + +• **T** + +## Parameters + +### callback + +`Callback`\<`T`\> + +## Returns + +`T` diff --git a/docs/reference/functions/setcurrentinstance.md b/docs/reference/functions/setcurrentinstance.md new file mode 100644 index 0000000..3acc0c4 --- /dev/null +++ b/docs/reference/functions/setcurrentinstance.md @@ -0,0 +1,24 @@ +--- +id: setCurrentInstance +title: setCurrentInstance +--- + + + +# Function: setCurrentInstance() + +```ts +function setCurrentInstance(newInstance): void +``` + +Defined in: [helpers.ts:52](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/helpers.ts#L52) + +## Parameters + +### newInstance + +`TestInstance` + +## Returns + +`void` diff --git a/docs/reference/functions/waitfor.md b/docs/reference/functions/waitfor.md new file mode 100644 index 0000000..bb4423b --- /dev/null +++ b/docs/reference/functions/waitfor.md @@ -0,0 +1,32 @@ +--- +id: waitFor +title: waitFor +--- + + + +# Function: waitFor() + +```ts +function waitFor(callback, options?): Promise +``` + +Defined in: [wait-for.ts:196](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/wait-for.ts#L196) + +## Type Parameters + +• **T** + +## Parameters + +### callback + +() => `T` \| `Promise`\<`T`\> + +### options? + +[`waitForOptions`](../interfaces/waitforoptions.md) + +## Returns + +`Promise`\<`T`\> diff --git a/docs/reference/functions/wrapsinglequerywithsuggestion.md b/docs/reference/functions/wrapsinglequerywithsuggestion.md new file mode 100644 index 0000000..3077442 --- /dev/null +++ b/docs/reference/functions/wrapsinglequerywithsuggestion.md @@ -0,0 +1,57 @@ +--- +id: wrapSingleQueryWithSuggestion +title: wrapSingleQueryWithSuggestion +--- + + + +# Function: wrapSingleQueryWithSuggestion() + +```ts +function wrapSingleQueryWithSuggestion( + query, + queryByName, + variant): (container, ...args) => T +``` + +Defined in: [query-helpers.ts:89](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/query-helpers.ts#L89) + +## Type Parameters + +• **TArguments** *extends* `unknown`[] + +## Parameters + +### query + +(`container`, ...`args`) => `null` \| `TestInstance` + +### queryByName + +`string` + +### variant + +`Variant` + +## Returns + +`Function` + +### Type Parameters + +• **T** *extends* `TestInstance` = `TestInstance` + +### Parameters + +#### container + +`TestInstance` + +#### args + +...`TArguments` + +### Returns + +`T` diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 0000000..fada7f2 --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,141 @@ +--- +id: cli-testing-library +title: cli-testing-library +--- + + + +# cli-testing-library + +## References + +### findByError + +Re-exports [findByError](namespaces/queries/functions/findbyerror.md) + +*** + +### FindByError + +Re-exports [FindByError](namespaces/queries/type-aliases/findbyerror.md) + +*** + +### findByText + +Re-exports [findByText](namespaces/queries/functions/findbytext.md) + +*** + +### FindByText + +Re-exports [FindByText](namespaces/queries/type-aliases/findbytext.md) + +*** + +### getByError + +Re-exports [getByError](namespaces/queries/functions/getbyerror.md) + +*** + +### GetByError + +Re-exports [GetByError](namespaces/queries/type-aliases/getbyerror.md) + +*** + +### getByText + +Re-exports [getByText](namespaces/queries/functions/getbytext.md) + +*** + +### GetByText + +Re-exports [GetByText](namespaces/queries/type-aliases/getbytext.md) + +*** + +### queryByError + +Re-exports [queryByError](namespaces/queries/functions/querybyerror.md) + +*** + +### QueryByError + +Re-exports [QueryByError](namespaces/queries/type-aliases/querybyerror.md) + +*** + +### queryByText + +Re-exports [queryByText](namespaces/queries/functions/querybytext.md) + +*** + +### QueryByText + +Re-exports [QueryByText](namespaces/queries/type-aliases/querybytext.md) + +## Namespaces + +- [queries](namespaces/queries/index.md) + +## Classes + +- [MutationObserver](classes/mutationobserver.md) + +## Interfaces + +- [Config](interfaces/config.md) +- [ConfigFn](interfaces/configfn.md) +- [DefaultNormalizerOptions](interfaces/defaultnormalizeroptions.md) +- [keyboardKey](interfaces/keyboardkey.md) +- [MatcherOptions](interfaces/matcheroptions.md) +- [NormalizerOptions](interfaces/normalizeroptions.md) +- [Queries](interfaces/queries.md) +- [RenderOptions](interfaces/renderoptions.md) +- [SelectorMatcherOptions](interfaces/selectormatcheroptions.md) +- [waitForOptions](interfaces/waitforoptions.md) + +## Type Aliases + +- [BoundFunction](type-aliases/boundfunction.md) +- [BoundFunctions](type-aliases/boundfunctions.md) +- [EventType](type-aliases/eventtype.md) +- [FireFunction](type-aliases/firefunction.md) +- [FireObject](type-aliases/fireobject.md) +- [GetErrorFunction](type-aliases/geterrorfunction.md) +- [Match](type-aliases/match.md) +- [Matcher](type-aliases/matcher.md) +- [MatcherFunction](type-aliases/matcherfunction.md) +- [NormalizerFn](type-aliases/normalizerfn.md) +- [Query](type-aliases/query.md) +- [QueryMethod](type-aliases/querymethod.md) +- [RenderResult](type-aliases/renderresult.md) +- [WithSuggest](type-aliases/withsuggest.md) + +## Functions + +- [\_runObservers](functions/runobservers.md) +- [bindObjectFnsToInstance](functions/bindobjectfnstoinstance.md) +- [buildQueries](functions/buildqueries.md) +- [cleanup](functions/cleanup.md) +- [configure](functions/configure.md) +- [debounce](functions/debounce.md) +- [fireEvent](functions/fireevent.md) +- [getConfig](functions/getconfig.md) +- [getCurrentInstance](functions/getcurrentinstance.md) +- [getDefaultNormalizer](functions/getdefaultnormalizer.md) +- [getInstanceError](functions/getinstanceerror.md) +- [getQueriesForElement](functions/getqueriesforelement.md) +- [jestFakeTimersAreEnabled](functions/jestfaketimersareenabled.md) +- [makeFindQuery](functions/makefindquery.md) +- [makeNormalizer](functions/makenormalizer.md) +- [render](functions/render.md) +- [runWithExpensiveErrorDiagnosticsDisabled](functions/runwithexpensiveerrordiagnosticsdisabled.md) +- [setCurrentInstance](functions/setcurrentinstance.md) +- [waitFor](functions/waitfor.md) +- [wrapSingleQueryWithSuggestion](functions/wrapsinglequerywithsuggestion.md) diff --git a/docs/reference/interfaces/config.md b/docs/reference/interfaces/config.md new file mode 100644 index 0000000..97837de --- /dev/null +++ b/docs/reference/interfaces/config.md @@ -0,0 +1,106 @@ +--- +id: Config +title: Config +--- + + + +# Interface: Config + +Defined in: [config.ts:5](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/config.ts#L5) + +## Properties + +### asyncUtilTimeout + +```ts +asyncUtilTimeout: number; +``` + +Defined in: [config.ts:13](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/config.ts#L13) + +*** + +### errorDebounceTimeout + +```ts +errorDebounceTimeout: number; +``` + +Defined in: [config.ts:15](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/config.ts#L15) + +*** + +### getInstanceError() + +```ts +getInstanceError: (message, container) => Error; +``` + +Defined in: [config.ts:18](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/config.ts#L18) + +#### Parameters + +##### message + +`null` | `string` + +##### container + +`TestInstance` + +#### Returns + +`Error` + +*** + +### renderAwaitTime + +```ts +renderAwaitTime: number; +``` + +Defined in: [config.ts:14](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/config.ts#L14) + +*** + +### showOriginalStackTrace + +```ts +showOriginalStackTrace: boolean; +``` + +Defined in: [config.ts:16](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/config.ts#L16) + +*** + +### throwSuggestions + +```ts +throwSuggestions: boolean; +``` + +Defined in: [config.ts:17](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/config.ts#L17) + +*** + +### unstable\_advanceTimersWrapper() + +```ts +unstable_advanceTimersWrapper: (cb) => unknown; +``` + +Defined in: [config.ts:10](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/config.ts#L10) + +WARNING: `unstable` prefix means this API may change in patch and minor releases. + +#### Parameters + +##### cb + +(...`args`) => `unknown` + +#### Returns + +`unknown` diff --git a/docs/reference/interfaces/configfn.md b/docs/reference/interfaces/configfn.md new file mode 100644 index 0000000..563678b --- /dev/null +++ b/docs/reference/interfaces/configfn.md @@ -0,0 +1,26 @@ +--- +id: ConfigFn +title: ConfigFn +--- + + + +# Interface: ConfigFn() + +Defined in: [config.ts:21](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/config.ts#L21) + +```ts +interface ConfigFn(existingConfig): Partial +``` + +Defined in: [config.ts:22](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/config.ts#L22) + +## Parameters + +### existingConfig + +[`Config`](config.md) + +## Returns + +`Partial`\<[`Config`](config.md)\> diff --git a/docs/reference/interfaces/defaultnormalizeroptions.md b/docs/reference/interfaces/defaultnormalizeroptions.md new file mode 100644 index 0000000..f9b0852 --- /dev/null +++ b/docs/reference/interfaces/defaultnormalizeroptions.md @@ -0,0 +1,44 @@ +--- +id: DefaultNormalizerOptions +title: DefaultNormalizerOptions +--- + + + +# Interface: DefaultNormalizerOptions + +Defined in: [matches.ts:37](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L37) + +## Extended by + +- [`NormalizerOptions`](normalizeroptions.md) + +## Properties + +### collapseWhitespace? + +```ts +optional collapseWhitespace: boolean; +``` + +Defined in: [matches.ts:39](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L39) + +*** + +### stripAnsi? + +```ts +optional stripAnsi: boolean; +``` + +Defined in: [matches.ts:40](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L40) + +*** + +### trim? + +```ts +optional trim: boolean; +``` + +Defined in: [matches.ts:38](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L38) diff --git a/docs/reference/interfaces/keyboardkey.md b/docs/reference/interfaces/keyboardkey.md new file mode 100644 index 0000000..51ed3e7 --- /dev/null +++ b/docs/reference/interfaces/keyboardkey.md @@ -0,0 +1,34 @@ +--- +id: keyboardKey +title: keyboardKey +--- + + + +# Interface: keyboardKey + +Defined in: [user-event/keyboard/types.ts:8](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/user-event/keyboard/types.ts#L8) + +## Properties + +### code? + +```ts +optional code: string; +``` + +Defined in: [user-event/keyboard/types.ts:10](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/user-event/keyboard/types.ts#L10) + +Physical location on a keyboard + +*** + +### hex? + +```ts +optional hex: string; +``` + +Defined in: [user-event/keyboard/types.ts:12](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/user-event/keyboard/types.ts#L12) + +Character or functional key hex code diff --git a/docs/reference/interfaces/matcheroptions.md b/docs/reference/interfaces/matcheroptions.md new file mode 100644 index 0000000..16e4141 --- /dev/null +++ b/docs/reference/interfaces/matcheroptions.md @@ -0,0 +1,82 @@ +--- +id: MatcherOptions +title: MatcherOptions +--- + + + +# Interface: MatcherOptions + +Defined in: [matches.ts:17](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L17) + +## Extended by + +- [`SelectorMatcherOptions`](selectormatcheroptions.md) + +## Properties + +### collapseWhitespace? + +```ts +optional collapseWhitespace: boolean; +``` + +Defined in: [matches.ts:24](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L24) + +Use normalizer with getDefaultNormalizer instead + +*** + +### exact? + +```ts +optional exact: boolean; +``` + +Defined in: [matches.ts:18](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L18) + +*** + +### normalizer? + +```ts +optional normalizer: NormalizerFn; +``` + +Defined in: [matches.ts:25](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L25) + +*** + +### stripAnsi? + +```ts +optional stripAnsi: boolean; +``` + +Defined in: [matches.ts:22](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L22) + +Use normalizer with getDefaultNormalizer instead + +*** + +### suggest? + +```ts +optional suggest: boolean; +``` + +Defined in: [matches.ts:27](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L27) + +suppress suggestions for a specific query + +*** + +### trim? + +```ts +optional trim: boolean; +``` + +Defined in: [matches.ts:20](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L20) + +Use normalizer with getDefaultNormalizer instead diff --git a/docs/reference/interfaces/normalizeroptions.md b/docs/reference/interfaces/normalizeroptions.md new file mode 100644 index 0000000..ea4d267 --- /dev/null +++ b/docs/reference/interfaces/normalizeroptions.md @@ -0,0 +1,66 @@ +--- +id: NormalizerOptions +title: NormalizerOptions +--- + + + +# Interface: NormalizerOptions + +Defined in: [matches.ts:13](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L13) + +## Extends + +- [`DefaultNormalizerOptions`](defaultnormalizeroptions.md) + +## Properties + +### collapseWhitespace? + +```ts +optional collapseWhitespace: boolean; +``` + +Defined in: [matches.ts:39](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L39) + +#### Inherited from + +[`DefaultNormalizerOptions`](defaultnormalizeroptions.md).[`collapseWhitespace`](DefaultNormalizerOptions.md#collapsewhitespace) + +*** + +### normalizer? + +```ts +optional normalizer: NormalizerFn; +``` + +Defined in: [matches.ts:14](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L14) + +*** + +### stripAnsi? + +```ts +optional stripAnsi: boolean; +``` + +Defined in: [matches.ts:40](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L40) + +#### Inherited from + +[`DefaultNormalizerOptions`](defaultnormalizeroptions.md).[`stripAnsi`](DefaultNormalizerOptions.md#stripansi) + +*** + +### trim? + +```ts +optional trim: boolean; +``` + +Defined in: [matches.ts:38](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L38) + +#### Inherited from + +[`DefaultNormalizerOptions`](defaultnormalizeroptions.md).[`trim`](DefaultNormalizerOptions.md#trim) diff --git a/docs/reference/interfaces/queries.md b/docs/reference/interfaces/queries.md new file mode 100644 index 0000000..fd33e3b --- /dev/null +++ b/docs/reference/interfaces/queries.md @@ -0,0 +1,16 @@ +--- +id: Queries +title: Queries +--- + + + +# Interface: Queries + +Defined in: [get-queries-for-instance.ts:40](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/get-queries-for-instance.ts#L40) + +## Indexable + +```ts +[T: string]: Query +``` diff --git a/docs/reference/interfaces/renderoptions.md b/docs/reference/interfaces/renderoptions.md new file mode 100644 index 0000000..8b4e88a --- /dev/null +++ b/docs/reference/interfaces/renderoptions.md @@ -0,0 +1,40 @@ +--- +id: RenderOptions +title: RenderOptions +--- + + + +# Interface: RenderOptions + +Defined in: [pure.ts:24](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/pure.ts#L24) + +## Properties + +### cwd + +```ts +cwd: string; +``` + +Defined in: [pure.ts:25](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/pure.ts#L25) + +*** + +### debug + +```ts +debug: boolean; +``` + +Defined in: [pure.ts:26](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/pure.ts#L26) + +*** + +### spawnOpts + +```ts +spawnOpts: Omit; +``` + +Defined in: [pure.ts:27](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/pure.ts#L27) diff --git a/docs/reference/interfaces/selectormatcheroptions.md b/docs/reference/interfaces/selectormatcheroptions.md new file mode 100644 index 0000000..82fe404 --- /dev/null +++ b/docs/reference/interfaces/selectormatcheroptions.md @@ -0,0 +1,126 @@ +--- +id: SelectorMatcherOptions +title: SelectorMatcherOptions +--- + + + +# Interface: SelectorMatcherOptions + +Defined in: [query-helpers.ts:16](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/query-helpers.ts#L16) + +## Extends + +- [`MatcherOptions`](matcheroptions.md) + +## Properties + +### collapseWhitespace? + +```ts +optional collapseWhitespace: boolean; +``` + +Defined in: [matches.ts:24](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L24) + +Use normalizer with getDefaultNormalizer instead + +#### Inherited from + +[`MatcherOptions`](matcheroptions.md).[`collapseWhitespace`](MatcherOptions.md#collapsewhitespace) + +*** + +### exact? + +```ts +optional exact: boolean; +``` + +Defined in: [matches.ts:18](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L18) + +#### Inherited from + +[`MatcherOptions`](matcheroptions.md).[`exact`](MatcherOptions.md#exact) + +*** + +### ignore? + +```ts +optional ignore: string | boolean; +``` + +Defined in: [query-helpers.ts:18](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/query-helpers.ts#L18) + +*** + +### normalizer? + +```ts +optional normalizer: NormalizerFn; +``` + +Defined in: [matches.ts:25](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L25) + +#### Inherited from + +[`MatcherOptions`](matcheroptions.md).[`normalizer`](MatcherOptions.md#normalizer) + +*** + +### selector? + +```ts +optional selector: string; +``` + +Defined in: [query-helpers.ts:17](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/query-helpers.ts#L17) + +*** + +### stripAnsi? + +```ts +optional stripAnsi: boolean; +``` + +Defined in: [matches.ts:22](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L22) + +Use normalizer with getDefaultNormalizer instead + +#### Inherited from + +[`MatcherOptions`](matcheroptions.md).[`stripAnsi`](MatcherOptions.md#stripansi) + +*** + +### suggest? + +```ts +optional suggest: boolean; +``` + +Defined in: [matches.ts:27](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L27) + +suppress suggestions for a specific query + +#### Inherited from + +[`MatcherOptions`](matcheroptions.md).[`suggest`](MatcherOptions.md#suggest) + +*** + +### trim? + +```ts +optional trim: boolean; +``` + +Defined in: [matches.ts:20](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L20) + +Use normalizer with getDefaultNormalizer instead + +#### Inherited from + +[`MatcherOptions`](matcheroptions.md).[`trim`](MatcherOptions.md#trim) diff --git a/docs/reference/interfaces/waitforoptions.md b/docs/reference/interfaces/waitforoptions.md new file mode 100644 index 0000000..f90cce4 --- /dev/null +++ b/docs/reference/interfaces/waitforoptions.md @@ -0,0 +1,80 @@ +--- +id: waitForOptions +title: waitForOptions +--- + + + +# Interface: waitForOptions + +Defined in: [wait-for.ts:14](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/wait-for.ts#L14) + +## Properties + +### instance? + +```ts +optional instance: TestInstance; +``` + +Defined in: [wait-for.ts:15](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/wait-for.ts#L15) + +*** + +### interval? + +```ts +optional interval: number; +``` + +Defined in: [wait-for.ts:18](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/wait-for.ts#L18) + +*** + +### onTimeout()? + +```ts +optional onTimeout: (error) => Error; +``` + +Defined in: [wait-for.ts:19](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/wait-for.ts#L19) + +#### Parameters + +##### error + +`Error` + +#### Returns + +`Error` + +*** + +### showOriginalStackTrace? + +```ts +optional showOriginalStackTrace: boolean; +``` + +Defined in: [wait-for.ts:16](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/wait-for.ts#L16) + +*** + +### stackTraceError? + +```ts +optional stackTraceError: Error; +``` + +Defined in: [wait-for.ts:20](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/wait-for.ts#L20) + +*** + +### timeout? + +```ts +optional timeout: number; +``` + +Defined in: [wait-for.ts:17](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/wait-for.ts#L17) diff --git a/docs/reference/namespaces/queries/functions/findbyerror.md b/docs/reference/namespaces/queries/functions/findbyerror.md new file mode 100644 index 0000000..deada4b --- /dev/null +++ b/docs/reference/namespaces/queries/functions/findbyerror.md @@ -0,0 +1,28 @@ +--- +id: findByError +title: findByError +--- + + + +# Function: findByError() + +```ts +function findByError(...args): ReturnType> +``` + +Defined in: [queries/error.ts:71](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/queries/error.ts#L71) + +## Type Parameters + +• **T** *extends* `TestInstance` = `TestInstance` + +## Parameters + +### args + +...\[`TestInstance`, [`Matcher`](../../../type-aliases/matcher.md), [`SelectorMatcherOptions`](../../../interfaces/selectormatcheroptions.md), [`waitForOptions`](../../../interfaces/waitforoptions.md)\] + +## Returns + +`ReturnType`\<[`FindByError`](../type-aliases/findbyerror.md)\<`T`\>\> diff --git a/docs/reference/namespaces/queries/functions/findbytext.md b/docs/reference/namespaces/queries/functions/findbytext.md new file mode 100644 index 0000000..06ac6b8 --- /dev/null +++ b/docs/reference/namespaces/queries/functions/findbytext.md @@ -0,0 +1,28 @@ +--- +id: findByText +title: findByText +--- + + + +# Function: findByText() + +```ts +function findByText(...args): ReturnType> +``` + +Defined in: [queries/text.ts:69](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/queries/text.ts#L69) + +## Type Parameters + +• **T** *extends* `TestInstance` = `TestInstance` + +## Parameters + +### args + +...\[`TestInstance`, [`Matcher`](../../../type-aliases/matcher.md), [`SelectorMatcherOptions`](../../../interfaces/selectormatcheroptions.md), [`waitForOptions`](../../../interfaces/waitforoptions.md)\] + +## Returns + +`ReturnType`\<[`FindByText`](../type-aliases/findbytext.md)\<`T`\>\> diff --git a/docs/reference/namespaces/queries/functions/getbyerror.md b/docs/reference/namespaces/queries/functions/getbyerror.md new file mode 100644 index 0000000..084c41f --- /dev/null +++ b/docs/reference/namespaces/queries/functions/getbyerror.md @@ -0,0 +1,28 @@ +--- +id: getByError +title: getByError +--- + + + +# Function: getByError() + +```ts +function getByError(...args): ReturnType> +``` + +Defined in: [queries/error.ts:59](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/queries/error.ts#L59) + +## Type Parameters + +• **T** *extends* `TestInstance` = `TestInstance` + +## Parameters + +### args + +...\[`TestInstance`, [`Matcher`](../../../type-aliases/matcher.md), [`SelectorMatcherOptions`](../../../interfaces/selectormatcheroptions.md)\] + +## Returns + +`ReturnType`\<[`GetByError`](../type-aliases/getbyerror.md)\<`T`\>\> diff --git a/docs/reference/namespaces/queries/functions/getbytext.md b/docs/reference/namespaces/queries/functions/getbytext.md new file mode 100644 index 0000000..0b648df --- /dev/null +++ b/docs/reference/namespaces/queries/functions/getbytext.md @@ -0,0 +1,28 @@ +--- +id: getByText +title: getByText +--- + + + +# Function: getByText() + +```ts +function getByText(...args): ReturnType> +``` + +Defined in: [queries/text.ts:59](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/queries/text.ts#L59) + +## Type Parameters + +• **T** *extends* `TestInstance` = `TestInstance` + +## Parameters + +### args + +...\[`TestInstance`, [`Matcher`](../../../type-aliases/matcher.md), [`SelectorMatcherOptions`](../../../interfaces/selectormatcheroptions.md)\] + +## Returns + +`ReturnType`\<[`GetByText`](../type-aliases/getbytext.md)\<`T`\>\> diff --git a/docs/reference/namespaces/queries/functions/querybyerror.md b/docs/reference/namespaces/queries/functions/querybyerror.md new file mode 100644 index 0000000..409756a --- /dev/null +++ b/docs/reference/namespaces/queries/functions/querybyerror.md @@ -0,0 +1,28 @@ +--- +id: queryByError +title: queryByError +--- + + + +# Function: queryByError() + +```ts +function queryByError(...args): ReturnType> +``` + +Defined in: [queries/error.ts:65](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/queries/error.ts#L65) + +## Type Parameters + +• **T** *extends* `TestInstance` = `TestInstance` + +## Parameters + +### args + +...\[`TestInstance`, [`Matcher`](../../../type-aliases/matcher.md), [`SelectorMatcherOptions`](../../../interfaces/selectormatcheroptions.md)\] + +## Returns + +`ReturnType`\<[`QueryByError`](../type-aliases/querybyerror.md)\<`T`\>\> diff --git a/docs/reference/namespaces/queries/functions/querybytext.md b/docs/reference/namespaces/queries/functions/querybytext.md new file mode 100644 index 0000000..28440e0 --- /dev/null +++ b/docs/reference/namespaces/queries/functions/querybytext.md @@ -0,0 +1,28 @@ +--- +id: queryByText +title: queryByText +--- + + + +# Function: queryByText() + +```ts +function queryByText(...args): ReturnType> +``` + +Defined in: [queries/text.ts:64](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/queries/text.ts#L64) + +## Type Parameters + +• **T** *extends* `TestInstance` = `TestInstance` + +## Parameters + +### args + +...\[`TestInstance`, [`Matcher`](../../../type-aliases/matcher.md), [`SelectorMatcherOptions`](../../../interfaces/selectormatcheroptions.md)\] + +## Returns + +`ReturnType`\<[`QueryByText`](../type-aliases/querybytext.md)\<`T`\>\> diff --git a/docs/reference/namespaces/queries/index.md b/docs/reference/namespaces/queries/index.md new file mode 100644 index 0000000..005ccdc --- /dev/null +++ b/docs/reference/namespaces/queries/index.md @@ -0,0 +1,26 @@ +--- +id: queries +title: queries +--- + + + +# queries + +## Type Aliases + +- [FindByError](type-aliases/findbyerror.md) +- [FindByText](type-aliases/findbytext.md) +- [GetByError](type-aliases/getbyerror.md) +- [GetByText](type-aliases/getbytext.md) +- [QueryByError](type-aliases/querybyerror.md) +- [QueryByText](type-aliases/querybytext.md) + +## Functions + +- [findByError](functions/findbyerror.md) +- [findByText](functions/findbytext.md) +- [getByError](functions/getbyerror.md) +- [getByText](functions/getbytext.md) +- [queryByError](functions/querybyerror.md) +- [queryByText](functions/querybytext.md) diff --git a/docs/reference/namespaces/queries/type-aliases/findbyerror.md b/docs/reference/namespaces/queries/type-aliases/findbyerror.md new file mode 100644 index 0000000..0f4abb9 --- /dev/null +++ b/docs/reference/namespaces/queries/type-aliases/findbyerror.md @@ -0,0 +1,40 @@ +--- +id: FindByError +title: FindByError +--- + + + +# Type Alias: FindByError()\ + +```ts +type FindByError = (instance, id, options?, waitForElementOptions?) => Promise; +``` + +Defined in: [queries/error.ts:27](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/queries/error.ts#L27) + +## Type Parameters + +• **T** *extends* `TestInstance` = `TestInstance` + +## Parameters + +### instance + +`TestInstance` + +### id + +[`Matcher`](../../../type-aliases/matcher.md) + +### options? + +[`SelectorMatcherOptions`](../../../interfaces/selectormatcheroptions.md) + +### waitForElementOptions? + +[`waitForOptions`](../../../interfaces/waitforoptions.md) + +## Returns + +`Promise`\<`T`\> diff --git a/docs/reference/namespaces/queries/type-aliases/findbytext.md b/docs/reference/namespaces/queries/type-aliases/findbytext.md new file mode 100644 index 0000000..76b9ba8 --- /dev/null +++ b/docs/reference/namespaces/queries/type-aliases/findbytext.md @@ -0,0 +1,40 @@ +--- +id: FindByText +title: FindByText +--- + + + +# Type Alias: FindByText()\ + +```ts +type FindByText = (instance, id, options?, waitForElementOptions?) => Promise; +``` + +Defined in: [queries/text.ts:27](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/queries/text.ts#L27) + +## Type Parameters + +• **T** *extends* `TestInstance` = `TestInstance` + +## Parameters + +### instance + +`TestInstance` + +### id + +[`Matcher`](../../../type-aliases/matcher.md) + +### options? + +[`SelectorMatcherOptions`](../../../interfaces/selectormatcheroptions.md) + +### waitForElementOptions? + +[`waitForOptions`](../../../interfaces/waitforoptions.md) + +## Returns + +`Promise`\<`T`\> diff --git a/docs/reference/namespaces/queries/type-aliases/getbyerror.md b/docs/reference/namespaces/queries/type-aliases/getbyerror.md new file mode 100644 index 0000000..434aa8b --- /dev/null +++ b/docs/reference/namespaces/queries/type-aliases/getbyerror.md @@ -0,0 +1,36 @@ +--- +id: GetByError +title: GetByError +--- + + + +# Type Alias: GetByError()\ + +```ts +type GetByError = (instance, id, options?) => T; +``` + +Defined in: [queries/error.ts:21](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/queries/error.ts#L21) + +## Type Parameters + +• **T** *extends* `TestInstance` = `TestInstance` + +## Parameters + +### instance + +`TestInstance` + +### id + +[`Matcher`](../../../type-aliases/matcher.md) + +### options? + +[`SelectorMatcherOptions`](../../../interfaces/selectormatcheroptions.md) + +## Returns + +`T` diff --git a/docs/reference/namespaces/queries/type-aliases/getbytext.md b/docs/reference/namespaces/queries/type-aliases/getbytext.md new file mode 100644 index 0000000..3e4d0f2 --- /dev/null +++ b/docs/reference/namespaces/queries/type-aliases/getbytext.md @@ -0,0 +1,36 @@ +--- +id: GetByText +title: GetByText +--- + + + +# Type Alias: GetByText()\ + +```ts +type GetByText = (instance, id, options?) => T; +``` + +Defined in: [queries/text.ts:21](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/queries/text.ts#L21) + +## Type Parameters + +• **T** *extends* `TestInstance` = `TestInstance` + +## Parameters + +### instance + +`TestInstance` + +### id + +[`Matcher`](../../../type-aliases/matcher.md) + +### options? + +[`SelectorMatcherOptions`](../../../interfaces/selectormatcheroptions.md) + +## Returns + +`T` diff --git a/docs/reference/namespaces/queries/type-aliases/querybyerror.md b/docs/reference/namespaces/queries/type-aliases/querybyerror.md new file mode 100644 index 0000000..80d593a --- /dev/null +++ b/docs/reference/namespaces/queries/type-aliases/querybyerror.md @@ -0,0 +1,36 @@ +--- +id: QueryByError +title: QueryByError +--- + + + +# Type Alias: QueryByError()\ + +```ts +type QueryByError = (instance, id, options?) => T | null; +``` + +Defined in: [queries/error.ts:15](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/queries/error.ts#L15) + +## Type Parameters + +• **T** *extends* `TestInstance` = `TestInstance` + +## Parameters + +### instance + +`TestInstance` + +### id + +[`Matcher`](../../../type-aliases/matcher.md) + +### options? + +[`SelectorMatcherOptions`](../../../interfaces/selectormatcheroptions.md) + +## Returns + +`T` \| `null` diff --git a/docs/reference/namespaces/queries/type-aliases/querybytext.md b/docs/reference/namespaces/queries/type-aliases/querybytext.md new file mode 100644 index 0000000..4d51f8d --- /dev/null +++ b/docs/reference/namespaces/queries/type-aliases/querybytext.md @@ -0,0 +1,36 @@ +--- +id: QueryByText +title: QueryByText +--- + + + +# Type Alias: QueryByText()\ + +```ts +type QueryByText = (instance, id, options?) => T | null; +``` + +Defined in: [queries/text.ts:15](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/queries/text.ts#L15) + +## Type Parameters + +• **T** *extends* `TestInstance` = `TestInstance` + +## Parameters + +### instance + +`TestInstance` + +### id + +[`Matcher`](../../../type-aliases/matcher.md) + +### options? + +[`SelectorMatcherOptions`](../../../interfaces/selectormatcheroptions.md) + +## Returns + +`T` \| `null` diff --git a/docs/reference/type-aliases/boundfunction.md b/docs/reference/type-aliases/boundfunction.md new file mode 100644 index 0000000..9fa91de --- /dev/null +++ b/docs/reference/type-aliases/boundfunction.md @@ -0,0 +1,18 @@ +--- +id: BoundFunction +title: BoundFunction +--- + + + +# Type Alias: BoundFunction\ + +```ts +type BoundFunction = T extends (container, ...args) => infer R ? (...args) => R : never; +``` + +Defined in: [get-queries-for-instance.ts:4](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/get-queries-for-instance.ts#L4) + +## Type Parameters + +• **T** diff --git a/docs/reference/type-aliases/boundfunctions.md b/docs/reference/type-aliases/boundfunctions.md new file mode 100644 index 0000000..177271f --- /dev/null +++ b/docs/reference/type-aliases/boundfunctions.md @@ -0,0 +1,18 @@ +--- +id: BoundFunctions +title: BoundFunctions +--- + + + +# Type Alias: BoundFunctions\ + +```ts +type BoundFunctions = TQueries extends typeof queries ? object & { [P in keyof TQueries]: BoundFunction } : { [P in keyof TQueries]: BoundFunction }; +``` + +Defined in: [get-queries-for-instance.ts:11](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/get-queries-for-instance.ts#L11) + +## Type Parameters + +• **TQueries** diff --git a/docs/reference/type-aliases/eventtype.md b/docs/reference/type-aliases/eventtype.md new file mode 100644 index 0000000..97e2b2e --- /dev/null +++ b/docs/reference/type-aliases/eventtype.md @@ -0,0 +1,14 @@ +--- +id: EventType +title: EventType +--- + + + +# Type Alias: EventType + +```ts +type EventType = keyof EventMap; +``` + +Defined in: [events.ts:5](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/events.ts#L5) diff --git a/docs/reference/type-aliases/firefunction.md b/docs/reference/type-aliases/firefunction.md new file mode 100644 index 0000000..34678d4 --- /dev/null +++ b/docs/reference/type-aliases/firefunction.md @@ -0,0 +1,36 @@ +--- +id: FireFunction +title: FireFunction +--- + + + +# Type Alias: FireFunction() + +```ts +type FireFunction = (instance, event, options?) => boolean | Promise; +``` + +Defined in: [events.ts:7](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/events.ts#L7) + +## Type Parameters + +• **TEventType** *extends* [`EventType`](eventtype.md) + +## Parameters + +### instance + +`TestInstance` + +### event + +`TEventType` + +### options? + +`Parameters`\<`EventMap`\[`TEventType`\]\>\[`1`\] + +## Returns + +`boolean` \| `Promise`\<`void`\> diff --git a/docs/reference/type-aliases/fireobject.md b/docs/reference/type-aliases/fireobject.md new file mode 100644 index 0000000..98fd944 --- /dev/null +++ b/docs/reference/type-aliases/fireobject.md @@ -0,0 +1,14 @@ +--- +id: FireObject +title: FireObject +--- + + + +# Type Alias: FireObject + +```ts +type FireObject = { [K in EventType]: (instance: TestInstance, options?: Parameters[1]) => boolean | Promise }; +``` + +Defined in: [events.ts:13](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/events.ts#L13) diff --git a/docs/reference/type-aliases/geterrorfunction.md b/docs/reference/type-aliases/geterrorfunction.md new file mode 100644 index 0000000..4b6626a --- /dev/null +++ b/docs/reference/type-aliases/geterrorfunction.md @@ -0,0 +1,32 @@ +--- +id: GetErrorFunction +title: GetErrorFunction +--- + + + +# Type Alias: GetErrorFunction()\ + +```ts +type GetErrorFunction = (c, ...args) => string; +``` + +Defined in: [query-helpers.ts:11](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/query-helpers.ts#L11) + +## Type Parameters + +• **TArguments** *extends* `any`[] = \[`string`\] + +## Parameters + +### c + +`TestInstance` | `null` + +### args + +...`TArguments` + +## Returns + +`string` diff --git a/docs/reference/type-aliases/match.md b/docs/reference/type-aliases/match.md new file mode 100644 index 0000000..3271237 --- /dev/null +++ b/docs/reference/type-aliases/match.md @@ -0,0 +1,36 @@ +--- +id: Match +title: Match +--- + + + +# Type Alias: Match() + +```ts +type Match = (textToMatch, node, matcher, options?) => boolean; +``` + +Defined in: [matches.ts:30](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L30) + +## Parameters + +### textToMatch + +`string` + +### node + +`TestInstance` | `null` + +### matcher + +[`Matcher`](matcher.md) + +### options? + +[`MatcherOptions`](../interfaces/matcheroptions.md) + +## Returns + +`boolean` diff --git a/docs/reference/type-aliases/matcher.md b/docs/reference/type-aliases/matcher.md new file mode 100644 index 0000000..4fff396 --- /dev/null +++ b/docs/reference/type-aliases/matcher.md @@ -0,0 +1,14 @@ +--- +id: Matcher +title: Matcher +--- + + + +# Type Alias: Matcher + +```ts +type Matcher = MatcherFunction | RegExp | number | string; +``` + +Defined in: [matches.ts:9](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L9) diff --git a/docs/reference/type-aliases/matcherfunction.md b/docs/reference/type-aliases/matcherfunction.md new file mode 100644 index 0000000..828139f --- /dev/null +++ b/docs/reference/type-aliases/matcherfunction.md @@ -0,0 +1,28 @@ +--- +id: MatcherFunction +title: MatcherFunction +--- + + + +# Type Alias: MatcherFunction() + +```ts +type MatcherFunction = (content, element) => boolean; +``` + +Defined in: [matches.ts:4](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L4) + +## Parameters + +### content + +`string` + +### element + +`TestInstance` | `null` + +## Returns + +`boolean` diff --git a/docs/reference/type-aliases/normalizerfn.md b/docs/reference/type-aliases/normalizerfn.md new file mode 100644 index 0000000..cf5e34e --- /dev/null +++ b/docs/reference/type-aliases/normalizerfn.md @@ -0,0 +1,24 @@ +--- +id: NormalizerFn +title: NormalizerFn +--- + + + +# Type Alias: NormalizerFn() + +```ts +type NormalizerFn = (text) => string; +``` + +Defined in: [matches.ts:11](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/matches.ts#L11) + +## Parameters + +### text + +`string` + +## Returns + +`string` diff --git a/docs/reference/type-aliases/query.md b/docs/reference/type-aliases/query.md new file mode 100644 index 0000000..7353892 --- /dev/null +++ b/docs/reference/type-aliases/query.md @@ -0,0 +1,39 @@ +--- +id: Query +title: Query +--- + + + +# Type Alias: Query() + +```ts +type Query = (container, ...args) => + | Error + | TestInstance + | TestInstance[] + | Promise + | Promise + | null; +``` + +Defined in: [get-queries-for-instance.ts:29](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/get-queries-for-instance.ts#L29) + +## Parameters + +### container + +`TestInstance` + +### args + +...`any`[] + +## Returns + + \| `Error` + \| `TestInstance` + \| `TestInstance`[] + \| `Promise`\<`TestInstance`[]\> + \| `Promise`\<`TestInstance`\> + \| `null` diff --git a/docs/reference/type-aliases/querymethod.md b/docs/reference/type-aliases/querymethod.md new file mode 100644 index 0000000..25f0906 --- /dev/null +++ b/docs/reference/type-aliases/querymethod.md @@ -0,0 +1,34 @@ +--- +id: QueryMethod +title: QueryMethod +--- + + + +# Type Alias: QueryMethod()\ + +```ts +type QueryMethod = (container, ...args) => TReturn; +``` + +Defined in: [query-helpers.ts:21](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/query-helpers.ts#L21) + +## Type Parameters + +• **TArguments** *extends* `any`[] + +• **TReturn** + +## Parameters + +### container + +`TestInstance` + +### args + +...`TArguments` + +## Returns + +`TReturn` diff --git a/docs/reference/type-aliases/renderresult.md b/docs/reference/type-aliases/renderresult.md new file mode 100644 index 0000000..51c6836 --- /dev/null +++ b/docs/reference/type-aliases/renderresult.md @@ -0,0 +1,22 @@ +--- +id: RenderResult +title: RenderResult +--- + + + +# Type Alias: RenderResult + +```ts +type RenderResult = TestInstance & object & { [P in keyof typeof queries]: BoundFunction }; +``` + +Defined in: [pure.ts:32](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/pure.ts#L32) + +## Type declaration + +### userEvent + +```ts +userEvent: { [P in keyof UserEvent]: BoundFunction }; +``` diff --git a/docs/reference/type-aliases/withsuggest.md b/docs/reference/type-aliases/withsuggest.md new file mode 100644 index 0000000..f096a97 --- /dev/null +++ b/docs/reference/type-aliases/withsuggest.md @@ -0,0 +1,22 @@ +--- +id: WithSuggest +title: WithSuggest +--- + + + +# Type Alias: WithSuggest + +```ts +type WithSuggest = object; +``` + +Defined in: [query-helpers.ts:9](https://github.com/crutchcorn/cli-testing-library/blob/main/packages/cli-testing-library/src/query-helpers.ts#L9) + +## Type declaration + +### suggest? + +```ts +optional suggest: boolean; +``` diff --git a/docs/user-event.md b/docs/user-event.md index 528cdcc..7e8199b 100644 --- a/docs/user-event.md +++ b/docs/user-event.md @@ -1,16 +1,10 @@ +--- +title: "User Event" +--- + [`user-event`][gh] is a helper that provides more advanced simulation of CLI interactions than the [`fireEvent`](./fire-event) method. - - - -- [Import](#import) -- [API](#api) - - [`keyboard(instance, text, [options])`](#keyboardinstance-text-options) - - [Special characters](#special-characters) - - - ## Import `userEvent` can be used either as a global import or as returned from `render`: diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..6be1a1e --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,12 @@ +// @ts-check + +// @ts-ignore Needed due to moduleResolution Node vs Bundler +import { tanstackConfig } from "@tanstack/config/eslint"; + +export default [ + ...tanstackConfig, + { + name: "clitesting/temp", + rules: {}, + }, +]; diff --git a/extend-expect.d.ts b/extend-expect.d.ts deleted file mode 100644 index 2d121d6..0000000 --- a/extend-expect.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// TypeScript Version: 3.8 - -/// - -declare namespace jest { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface Matchers { - /** - * @description - * Assert whether a query is present in the console or not. - * @example - * expect(queryByText('Hello world')).toBeInTheDocument() - */ - toBeInTheConsole(): R - /** - * @description - * Check whether the given instance has an stderr message or not. - * @example - * expect(instance).toHaveErrorMessage(/command could not be found/i) // to partially match - */ - toHaveErrorMessage(): R - } -} diff --git a/extend-expect.js b/extend-expect.js deleted file mode 100644 index e7d19c1..0000000 --- a/extend-expect.js +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line -require('./dist/extend-expect') diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 32837d6..0000000 --- a/jest.config.js +++ /dev/null @@ -1,28 +0,0 @@ -const { - collectCoverageFrom, - coveragePathIgnorePatterns, - coverageThreshold, - watchPlugins, -} = require('kcd-scripts/jest') - -module.exports = { - collectCoverageFrom, - passWithNoTests: true, - coveragePathIgnorePatterns: [ - ...coveragePathIgnorePatterns, - '/__tests__/', - '/__node_tests__/', - ], - coverageThreshold: { - ...coverageThreshold, - // TODO: Remove this - global: { - branches: 40, - functions: 50, - lines: 50, - statements: 50, - }, - }, - watchPlugins, - projects: [require.resolve('./tests/jest.config.js')], -} diff --git a/knip.json b/knip.json new file mode 100644 index 0000000..313a472 --- /dev/null +++ b/knip.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://unpkg.com/knip@5/schema.json", + "ignore": ["**/tests/execute-scripts/*", "website/src/components/*"], + "ignoreDependencies": [ + "sharp", + "@babel/runtime", + "@jest/expect", + "inquirer", + "@types/inquirer", + "@jest/globals", + "vitest" + ], + "ignoreExportsUsedInFile": true +} diff --git a/other/koala.png b/media/koala.png similarity index 100% rename from other/koala.png rename to media/koala.png diff --git a/nx.json b/nx.json new file mode 100644 index 0000000..aa54c43 --- /dev/null +++ b/nx.json @@ -0,0 +1,60 @@ +{ + "$schema": "./node_modules/nx/schemas/nx-schema.json", + "defaultBase": "main", + "useInferencePlugins": false, + "parallel": 5, + "namedInputs": { + "sharedGlobals": [ + "{workspaceRoot}/.nvmrc", + "{workspaceRoot}/package.json", + "{workspaceRoot}/tsconfig.json" + ], + "default": [ + "sharedGlobals", + "{projectRoot}/**/*", + "!{projectRoot}/**/*.md" + ], + "production": [ + "default", + "!{projectRoot}/tests/**/*", + "!{projectRoot}/eslint.config.js" + ] + }, + "targetDefaults": { + "test:knip": { + "cache": true, + "inputs": ["{workspaceRoot}/**/*"] + }, + "test:sherif": { + "cache": true, + "inputs": ["{workspaceRoot}/**/package.json"] + }, + "test:eslint": { + "cache": true, + "dependsOn": ["^build"], + "inputs": ["default", "^production", "{workspaceRoot}/eslint.config.js"] + }, + "test:lib": { + "cache": true, + "dependsOn": ["^build"], + "inputs": ["default", "^production"], + "outputs": ["{projectRoot}/coverage"] + }, + "test:types": { + "cache": true, + "dependsOn": ["^build"], + "inputs": ["default", "^production"] + }, + "build": { + "cache": true, + "dependsOn": ["^build"], + "inputs": ["production", "^production"], + "outputs": ["{projectRoot}/build", "{projectRoot}/dist"] + }, + "test:build": { + "cache": true, + "dependsOn": ["build"], + "inputs": ["production"] + } + } +} diff --git a/other/MAINTAINING.md b/other/MAINTAINING.md deleted file mode 100644 index a2f1a47..0000000 --- a/other/MAINTAINING.md +++ /dev/null @@ -1,84 +0,0 @@ -# Maintaining - - - - -**Table of Contents** - -- [Code of Conduct](#code-of-conduct) -- [Issues](#issues) -- [Pull Requests](#pull-requests) -- [Release](#release) -- [Thanks!](#thanks) - - - -This is documentation for maintainers of this project. - -## Code of Conduct - -Please review, understand, and be an example of it. Violations of the code of -conduct are taken seriously, even (especially) for maintainers. - -## Issues - -We want to support and build the community. We do that best by helping people -learn to solve their own problems. We have an issue template and hopefully most -folks follow it. If it's not clear what the issue is, invite them to create a -minimal reproduction of what they're trying to accomplish or the bug they think -they've found. - -Once it's determined that a code change is necessary, point people to -[makeapullrequest.com](http://makeapullrequest.com) and invite them to make a -pull request. If they're the one who needs the feature, they're the one who can -build it. If they need some hand holding and you have time to lend a hand, -please do so. It's an investment into another human being, and an investment -into a potential maintainer. - -Remember that this is open source, so the code is not yours, it's ours. If -someone needs a change in the codebase, you don't have to make it happen -yourself. Commit as much time to the project as you want/need to. Nobody can ask -any more of you than that. - -## Pull Requests - -As a maintainer, you're fine to make your branches on the main repo or on your -own fork. Either way is fine. - -When we receive a pull request, a GitHub Action is kicked off automatically (see -the `.github/workflows/validate.yml` for what runs in the Action). We avoid -merging anything that breaks the GitHub Action. - -Please review PRs and focus on the code rather than the individual. You never -know when this is someone's first ever PR and we want their experience to be as -positive as possible, so be uplifting and constructive. - -When you merge the pull request, 99% of the time you should use the -[Squash and merge](https://help.github.com/articles/merging-a-pull-request/) -feature. This keeps our git history clean, but more importantly, this allows us -to make any necessary changes to the commit message so we release what we want -to release. See the next section on Releases for more about that. - -## Release - -Our releases are automatic. They happen whenever code lands into `main`. A -GitHub Action gets kicked off and if it's successful, a tool called -[`semantic-release`](https://github.com/semantic-release/semantic-release) is -used to automatically publish a new release to npm as well as a changelog to -GitHub. It is only able to determine the version and whether a release is -necessary by the git commit messages. With this in mind, **please brush up on -[the commit message convention][commit] which drives our releases.** - -> One important note about this: Please make sure that commit messages do NOT -> contain the words "BREAKING CHANGE" in them unless we want to push a major -> version. I've been burned by this more than once where someone will include -> "BREAKING CHANGE: None" and it will end up releasing a new major version. Not -> a huge deal honestly, but kind of annoying... - -## Thanks! - -Thank you so much for helping to maintain this project! - - -[commit]: https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md - diff --git a/other/USERS.md b/other/USERS.md deleted file mode 100644 index 4bc1281..0000000 --- a/other/USERS.md +++ /dev/null @@ -1,12 +0,0 @@ -# Users - -If you or your company uses this project, add your name to this list! Eventually -we may have a website to showcase these (wanna build it!?) - -> No users have been added yet! - - diff --git a/other/manual-releases.md b/other/manual-releases.md deleted file mode 100644 index e1e77e6..0000000 --- a/other/manual-releases.md +++ /dev/null @@ -1,44 +0,0 @@ -# manual-releases - -This project has an automated release set up. So things are only released when -there are useful changes in the code that justify a release. But sometimes -things get messed up one way or another and we need to trigger the release -ourselves. When this happens, simply bump the number below and commit that with -the following commit message based on your needs: - -**Major** - -``` -fix(release): manually release a major version - -There was an issue with a major release, so this manual-releases.md -change is to release a new major version. - -Reference: # - -BREAKING CHANGE: -``` - -**Minor** - -``` -feat(release): manually release a minor version - -There was an issue with a minor release, so this manual-releases.md -change is to release a new minor version. - -Reference: # -``` - -**Patch** - -``` -fix(release): manually release a patch version - -There was an issue with a patch release, so this manual-releases.md -change is to release a new patch version. - -Reference: # -``` - -The number of times we've had to do a manual release is: 6 diff --git a/package.json b/package.json index 3427919..485757c 100644 --- a/package.json +++ b/package.json @@ -1,109 +1,60 @@ { - "name": "cli-testing-library", - "version": "0.0.0-semantically-released", - "description": "Simple and complete CLI testing utilities that encourage good testing practices.", - "main": "dist/index.js", - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./dist/cli-testing-library.cjs.js", - "import": "./dist/cli-testing-library.esm.js" - }, - "./extend-expect": { - "types": "./extend-expect.d.ts", - "require": "./dist/extend-expect.js", - "import": "./dist/extend-expect.js" - } - }, - "types": "types/index.d.ts", - "module": "dist/cli-testing-library.esm.js", - "umd:main": "dist/cli-testing-library.umd.js", - "source": "src/index.js", - "keywords": [ - "testing", - "cli", - "unit", - "integration", - "functional", - "end-to-end", - "e2e" - ], - "author": "Corbin Crutchley (https://crutchcorn.com)", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "scripts": { - "build": "kcd-scripts build --no-ts-defs --ignore \"**/__tests__/**,**/__node_tests__/**,**/__mocks__/**\" && kcd-scripts build --no-ts-defs --bundle --no-clean", - "format": "kcd-scripts format", - "lint": "kcd-scripts lint", - "setup": "npm install && npm run validate -s", - "test": "kcd-scripts test", - "test:debug": "node --inspect-brk ./node_modules/.bin/jest --watch --runInBand", - "test:update": "npm test -- --updateSnapshot --coverage", - "validate": "kcd-scripts validate", - "typecheck": "kcd-scripts typecheck --build types" - }, - "files": [ - "dist", - "types/*.d.ts", - "types/user-event/**/*.d.ts", - "extend-expect.js", - "extend-expect.d.ts", - "src/user-event/**/*.ts", - "src/user-event/*.ts" - ], - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "jest-matcher-utils": "^27.4.2", - "lz-string": "^1.4.4", - "pretty-format": "^27.0.2", - "redent": "^3.0.0", - "slice-ansi": "^4.0.0", - "strip-ansi": "^6.0.1", - "strip-final-newline": "^2.0.0", - "tree-kill": "^1.2.2" - }, - "devDependencies": { - "@types/lz-string": "^1.3.34", - "@types/strip-final-newline": "^3.0.0", - "chalk": "^4.1.2", - "has-ansi": "^3.0.0", - "inquirer": "^8.2.0", - "jest-in-case": "^1.0.2", - "jest-watch-select-projects": "^2.0.0", - "kcd-scripts": "^12.2.0", - "typescript": "^4.7.2" - }, - "eslintConfig": { - "extends": [ - "./node_modules/kcd-scripts/eslint.js", - "plugin:import/typescript" - ], - "rules": { - "@typescript-eslint/prefer-includes": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "import/prefer-default-export": "off", - "import/no-unassigned-import": "off", - "import/no-useless-path-segments": "off", - "no-console": "off", - "import/consistent-type-specifier-style": "off" - } - }, - "eslintIgnore": [ - "node_modules", - "coverage", - "dist" - ], + "name": "root", + "private": true, "repository": { "type": "git", "url": "https://github.com/crutchcorn/cli-testing-library" }, - "bugs": { - "url": "https://github.com/crutchcorn/cli-testing-library/issues" + "packageManager": "pnpm@9.14.4", + "type": "module", + "scripts": { + "clean": "pnpm --filter \"./packages/**\" run clean", + "preinstall": "node -e \"if(process.env.CI == 'true') {console.log('Skipping preinstall...')} else {process.exit(1)}\" || npx -y only-allow pnpm", + "test": "pnpm run test:ci", + "test:pr": "nx run-many --targets=test:sherif,test:knip,test:eslint,test:lib,test:types,test:build,build", + "test:ci": "nx run-many --targets=test:sherif,test:knip,test:eslint,test:lib,test:types,test:build,build", + "test:eslint": "nx run-many --target=test:eslint", + "test:format": "pnpm run prettier --check", + "test:sherif": "sherif", + "test:lib": "nx run-many --target=test:lib --exclude=examples/**", + "test:lib:dev": "pnpm run test:lib && nx watch --all -- pnpm run test:lib", + "test:build": "nx run-many --target=test:build --exclude=examples/**", + "test:types": "nx run-many --target=test:types --exclude=examples/**", + "test:knip": "knip", + "build": "nx run-many --target=build --exclude=examples/**", + "build:website": "nx run-many --target=build --projects=website", + "build:all": "nx run-many --target=build --exclude=examples/**", + "watch": "pnpm run build:all && nx watch --all -- pnpm run build:all", + "dev": "pnpm run watch", + "prettier": "prettier --ignore-unknown .", + "prettier:write": "pnpm run prettier --write", + "docs:generate": "node scripts/generateDocs.js", + "cipublish": "node scripts/publish.js", + "cipublishforce": "CI=true pnpm cipublish" + }, + "nx": { + "includedScripts": [ + "test:knip", + "test:sherif" + ] }, - "homepage": "https://github.com/crutchcorn/cli-testing-library#readme" + "devDependencies": { + "@tanstack/config": "^0.16.1", + "@types/node": "^22.10.10", + "eslint": "9.19.0", + "knip": "^5.43.3", + "nx": "^20.3.3", + "premove": "^4.0.0", + "prettier": "^3.4.2", + "publint": "^0.3.2", + "sherif": "^1.2.0", + "typescript": "5.6.3", + "typescript51": "npm:typescript@5.1", + "typescript52": "npm:typescript@5.2", + "typescript53": "npm:typescript@5.3", + "typescript54": "npm:typescript@5.4", + "typescript55": "npm:typescript@5.5", + "vite": "^6.0.11", + "vitest": "^3.0.4" + } } diff --git a/packages/cli-testing-library/README.md b/packages/cli-testing-library/README.md new file mode 100644 index 0000000..200b2b4 --- /dev/null +++ b/packages/cli-testing-library/README.md @@ -0,0 +1,108 @@ +
+

CLI Testing Library

+ + + koala + + +

Simple and complete CLI testing utilities that encourage good testing +practices.

+ +
+ +
+ +[![Build Status](https://img.shields.io/github/actions/workflow/status/crutchcorn/cli-testing-library/validate.yml?branch=main&style=flat-square)](https://github.com/crutchcorn/cli-testing-library/actions/workflows/validate.yml?query=branch%3Amain) +[![version](https://img.shields.io/npm/v/cli-testing-library?style=flat-square)](https://www.npmjs.com/package/cli-testing-library) +[![downloads](https://img.shields.io/npm/dw/cli-testing-library?style=flat-square)](https://www.npmjs.com/package/cli-testing-library) +[![MIT License](https://img.shields.io/npm/l/cli-testing-library?style=flat-square)](./LICENSE) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](makeapullrequest.com) +[![Code of Conduct](https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square)](./CODE_OF_CONDUCT.md) + + + +[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-) + + + +[![Watch on GitHub](https://img.shields.io/github/watchers/crutchcorn/cli-testing-library.svg?style=social)](https://github.com/crutchcorn/cli-testing-library/watchers) +[![Star on GitHub](https://img.shields.io/github/stars/crutchcorn/cli-testing-library.svg?style=social)](https://github.com/crutchcorn/cli-testing-library/stargazers) + + + +## Table of Contents + + + + +- [Installation](#installation) +- [Usage](#usage) +- [Contributors ✨](#contributors-) + + + +> This project is not affiliated with the +> ["Testing Library"](https://github.com/testing-library) ecosystem that this +> project is clearly inspired from. We're just big fans :) + +## Installation + +This module is distributed via [npm][npm] which is bundled with [node][node] and +should be installed as one of your project's `devDependencies`: + +``` +npm install --save-dev cli-testing-library +``` + +## Usage + +> This is currently the only section of "usage" documentation. We'll be +> expanding it as soon as possible + +Usage example: + +```javascript +const { resolve } = require("path"); +const { render } = require("cli-testing-library"); + +test("Is able to make terminal input and view in-progress stdout", async () => { + const { clear, findByText, queryByText, userEvent } = await render("node", [ + resolve(__dirname, "./execute-scripts/stdio-inquirer.js"), + ]); + + const instance = await findByText("First option"); + + expect(instance).toBeInTheConsole(); + + expect(await findByText("❯ One")).toBeInTheConsole(); + + clear(); + + userEvent("[ArrowDown]"); + + expect(await findByText("❯ Two")).toBeInTheConsole(); + + clear(); + + userEvent.keyboard("[Enter]"); + + expect(await findByText("First option: Two")).toBeInTheConsole(); + expect(await queryByText("First option: Three")).not.toBeInTheConsole(); +}); +``` + +For a API reference documentation, including suggestions on how to use this +library, see our +[documentation introduction with further reading](./docs/introduction.md). + +> While this library _does_ work in Windows, it does not appear to function +> properly in Windows CI environments, such as GitHub actions. As a result, you +> may need to either switch CI systems or limit your CI to only run in Linux +> +> If you know how to fix this, please let us know in +> [this tracking issue](https://github.com/crutchcorn/cli-testing-library/issues/3) diff --git a/packages/cli-testing-library/eslint.config.js b/packages/cli-testing-library/eslint.config.js new file mode 100644 index 0000000..835e0f6 --- /dev/null +++ b/packages/cli-testing-library/eslint.config.js @@ -0,0 +1,5 @@ +// @ts-check + +import rootConfig from "../../eslint.config.js"; + +export default [...rootConfig]; diff --git a/packages/cli-testing-library/jest-globals/package.json b/packages/cli-testing-library/jest-globals/package.json new file mode 100644 index 0000000..cbfcfaf --- /dev/null +++ b/packages/cli-testing-library/jest-globals/package.json @@ -0,0 +1,25 @@ +{ + "name": "cli-testing-library-jest-globals", + "version": "3.0.0", + "description": "", + "type": "module", + "module": "./../dist/esm/jest-globals.js", + "main": "./../dist/cjs/jest-globals.cjs", + "types": "./../dist/cjs/jest-globals.d.cts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./../dist/esm/jest-globals.d.ts", + "default": "./../dist/esm/jest-globals.js" + }, + "require": { + "types": "./../dist/cjs/jest-globals.d.cts", + "default": "./../dist/cjs/jest-globals.cjs" + } + } + }, + "author": "Corbin Crutchley (https://crutchcorn.dev)", + "license": "MIT", + "sideEffects": true +} diff --git a/packages/cli-testing-library/jest/package.json b/packages/cli-testing-library/jest/package.json new file mode 100644 index 0000000..08fde4e --- /dev/null +++ b/packages/cli-testing-library/jest/package.json @@ -0,0 +1,25 @@ +{ + "name": "cli-testing-library-jest", + "version": "3.0.0", + "description": "", + "type": "module", + "module": "./../dist/esm/jest.js", + "main": "./../dist/cjs/jest.cjs", + "types": "./../dist/cjs/jest.d.cts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./../dist/esm/jest.d.ts", + "default": "./../dist/esm/jest.js" + }, + "require": { + "types": "./../dist/cjs/jest.d.cts", + "default": "./../dist/cjs/jest.cjs" + } + } + }, + "author": "Corbin Crutchley (https://crutchcorn.dev)", + "license": "MIT", + "sideEffects": true +} diff --git a/packages/cli-testing-library/package.json b/packages/cli-testing-library/package.json new file mode 100644 index 0000000..6abc7b3 --- /dev/null +++ b/packages/cli-testing-library/package.json @@ -0,0 +1,136 @@ +{ + "name": "cli-testing-library", + "version": "3.0.0", + "description": "Simple and complete CLI testing utilities that encourage good testing practices.", + "keywords": [ + "testing", + "cli", + "unit", + "integration", + "functional", + "end-to-end", + "e2e" + ], + "author": "Corbin Crutchley (https://crutchcorn.dev)", + "repository": { + "type": "git", + "url": "https://github.com/crutchcorn/cli-testing-library", + "directory": "packages/cli-testing-library" + }, + "bugs": { + "url": "https://github.com/crutchcorn/cli-testing-library/issues" + }, + "homepage": "https://github.com/crutchcorn/cli-testing-library#readme", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/crutchcorn" + }, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "scripts": { + "clean": "premove ./dist ./coverage", + "test:eslint": "eslint ./src ./tests", + "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"", + "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js", + "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js", + "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js", + "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js", + "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js", + "test:types:ts56": "tsc", + "test:lib": "vitest", + "test:lib:dev": "pnpm run test:lib --watch", + "test:build": "publint --strict", + "build": "vite build" + }, + "type": "module", + "types": "dist/esm/index.d.ts", + "main": "dist/cjs/index.cjs", + "module": "dist/esm/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + } + }, + "./jest": { + "import": { + "types": "./dist/esm/jest.d.ts", + "default": "./dist/esm/jest.js" + }, + "require": { + "types": "./dist/cjs/jest.d.cts", + "default": "./dist/cjs/jest.cjs" + } + }, + "./jest-globals": { + "import": { + "types": "./dist/esm/jest-globals.d.ts", + "default": "./dist/esm/jest-globals.js" + }, + "require": { + "types": "./dist/cjs/jest-globals.d.cts", + "default": "./dist/cjs/jest-globals.cjs" + } + }, + "./vitest": { + "import": { + "types": "./dist/esm/vitest.d.ts", + "default": "./dist/esm/vitest.js" + }, + "require": { + "types": "./dist/cjs/vitest.d.cts", + "default": "./dist/cjs/vitest.cjs" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "src", + "vitest", + "jest", + "jest-globals" + ], + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "picocolors": "^1.1.1", + "redent": "^4.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "strip-final-newline": "^4.0.0", + "tree-kill": "^1.2.2" + }, + "devDependencies": { + "@jest/expect": "^29.7.0", + "@jest/globals": "^29.7.0", + "@types/babel__code-frame": "^7.0.6", + "@types/inquirer": "^9.0.7", + "@vitest/coverage-istanbul": "3.0.4", + "inquirer": "^12.3.2" + }, + "peerDependencies": { + "@jest/expect": "^29.0.0", + "@jest/globals": "^29.0.0", + "vitest": "^3.0.0" + }, + "peerDependenciesMeta": { + "@jest/globals": { + "optional": true + }, + "@jest/expect": { + "optional": true + }, + "vitest": { + "optional": true + } + } +} diff --git a/src/config.ts b/packages/cli-testing-library/src/config.ts similarity index 55% rename from src/config.ts rename to packages/cli-testing-library/src/config.ts index 1337b5c..30dbda4 100644 --- a/src/config.ts +++ b/packages/cli-testing-library/src/config.ts @@ -1,10 +1,30 @@ -import {Config, ConfigFn} from '../types/config' -import {TestInstance} from '../types/pure' +import type { TestInstance } from "./types"; + // import {prettyDOM} from './pretty-dom' -type Callback = () => T +export interface Config { + /** + * WARNING: `unstable` prefix means this API may change in patch and minor releases. + * @param cb + */ + unstable_advanceTimersWrapper: ( + cb: (...args: Array) => unknown, + ) => unknown; + asyncUtilTimeout: number; + renderAwaitTime: number; + errorDebounceTimeout: number; + showOriginalStackTrace: boolean; + throwSuggestions: boolean; + getInstanceError: (message: string | null, container: TestInstance) => Error; +} + +export interface ConfigFn { + (existingConfig: Config): Partial; +} + +type Callback = () => T; interface InternalConfig extends Config { - _disableExpensiveErrorDiagnostics: boolean + _disableExpensiveErrorDiagnostics: boolean; } // It would be cleaner for this to live inside './queries', but @@ -17,7 +37,7 @@ let config: InternalConfig = { renderAwaitTime: 100, // Internal timer time to wait before attempting error recovery debounce action errorDebounceTimeout: 100, - unstable_advanceTimersWrapper: cb => cb(), + unstable_advanceTimersWrapper: (cb) => cb(), // default value for the `hidden` option in `ByRole` queries // showOriginalStackTrace flag to show the full error stack traces for async errors showOriginalStackTrace: false, @@ -27,47 +47,47 @@ let config: InternalConfig = { // called when getBy* queries fail. (message, container) => Error getInstanceError(message, testInstance: TestInstance | undefined) { - let instanceWarning: string = '' + let instanceWarning = ""; if (testInstance) { - const stdallArrStr = testInstance.getStdallStr() - instanceWarning = `\n${stdallArrStr}` + const stdallArrStr = testInstance.getStdallStr(); + instanceWarning = `\n${stdallArrStr}`; } else { - instanceWarning = '' + instanceWarning = ""; } const error = new Error( - [message, instanceWarning].filter(Boolean).join('\n\n'), - ) - error.name = 'TestingLibraryElementError' - return error + [message, instanceWarning].filter(Boolean).join("\n\n"), + ); + error.name = "TestingLibraryElementError"; + return error; }, _disableExpensiveErrorDiagnostics: false, -} +}; export function runWithExpensiveErrorDiagnosticsDisabled( callback: Callback, ) { try { - config._disableExpensiveErrorDiagnostics = true - return callback() + config._disableExpensiveErrorDiagnostics = true; + return callback(); } finally { - config._disableExpensiveErrorDiagnostics = false + config._disableExpensiveErrorDiagnostics = false; } } -export function configure(newConfig: ConfigFn | Partial) { - if (typeof newConfig === 'function') { +export function configure(newConfig: ConfigFn | Partial): void { + if (typeof newConfig === "function") { // Pass the existing config out to the provided function // and accept a delta in return - newConfig = newConfig(config) + newConfig = newConfig(config); } // Merge the incoming config delta config = { ...config, ...newConfig, - } + }; } export function getConfig(): Config { - return config + return config; } diff --git a/packages/cli-testing-library/src/event-map.ts b/packages/cli-testing-library/src/event-map.ts new file mode 100644 index 0000000..1edd6c1 --- /dev/null +++ b/packages/cli-testing-library/src/event-map.ts @@ -0,0 +1,15 @@ +import { killProc } from "./process-helpers"; +import type { TestInstance } from "./types"; + +const isWin = process.platform === "win32"; + +const eventMap = { + sigterm: (instance: TestInstance): Promise => + killProc(instance, isWin ? undefined : "SIGTERM"), + sigkill: (instance: TestInstance): Promise => + killProc(instance, isWin ? undefined : "SIGKILL"), + write: (instance: TestInstance, props: { value: string }): boolean => + instance.process.stdin.write(props.value), +}; + +export { eventMap }; diff --git a/packages/cli-testing-library/src/events.ts b/packages/cli-testing-library/src/events.ts new file mode 100644 index 0000000..48ee03e --- /dev/null +++ b/packages/cli-testing-library/src/events.ts @@ -0,0 +1,38 @@ +import { eventMap } from "./event-map"; +import type { TestInstance } from "./types"; + +type EventMap = typeof eventMap; +export type EventType = keyof EventMap; + +export type FireFunction = ( + instance: TestInstance, + event: TEventType, + options?: Parameters[1], +) => boolean | Promise; + +export type FireObject = { + [K in EventType]: ( + instance: TestInstance, + options?: Parameters[1], + ) => boolean | Promise; +}; + +const fireEvent: FireFunction & FireObject = (( + instance, + event, + props = undefined, +) => { + return eventMap[event](instance, props!); +}) satisfies FireFunction as never; + +Object.entries(eventMap).forEach(([_eventName, _eventFn]) => { + const eventName = _eventName as keyof typeof eventMap; + const eventFn = _eventFn as ( + ...props: Array + ) => ReturnType<(typeof eventMap)[keyof typeof eventMap]>; + fireEvent[eventName] = (instance, ...props) => { + return eventFn(instance, ...props); + }; +}); + +export { fireEvent }; diff --git a/packages/cli-testing-library/src/get-queries-for-instance.ts b/packages/cli-testing-library/src/get-queries-for-instance.ts new file mode 100644 index 0000000..5100e9e --- /dev/null +++ b/packages/cli-testing-library/src/get-queries-for-instance.ts @@ -0,0 +1,62 @@ +import * as defaultQueries from "./queries/index"; +import type { TestInstance } from "./types"; + +export type BoundFunction = T extends ( + container: TestInstance, + ...args: infer P +) => infer R + ? (...args: P) => R + : never; + +export type BoundFunctions = TQueries extends typeof defaultQueries + ? { + getByText: ( + ...args: Parameters>> + ) => ReturnType>; + queryByText: ( + ...args: Parameters>> + ) => ReturnType>; + findByText: ( + ...args: Parameters>> + ) => ReturnType>; + } & { + [P in keyof TQueries]: BoundFunction; + } + : { + [P in keyof TQueries]: BoundFunction; + }; + +export type Query = ( + container: TestInstance, + ...args: Array +) => + | Error + | TestInstance + | Array + | Promise> + | Promise + | null; + +export interface Queries { + [T: string]: Query; +} + +/** + * @param instance + * @param queries object of functions + * @param initialValue for reducer + * @returns returns object of functions bound to container + */ +function getQueriesForElement( + instance: TestInstance, + queries: T = defaultQueries as unknown as T, + initialValue = {}, +): BoundFunctions { + return Object.keys(queries).reduce((helpers, key) => { + const fn = queries[key]; + helpers[key] = fn!.bind(null, instance); + return helpers; + }, initialValue as BoundFunctions); +} + +export { getQueriesForElement }; diff --git a/packages/cli-testing-library/src/get-user-code-frame.ts b/packages/cli-testing-library/src/get-user-code-frame.ts new file mode 100644 index 0000000..0bfdacb --- /dev/null +++ b/packages/cli-testing-library/src/get-user-code-frame.ts @@ -0,0 +1,57 @@ +// We try to load node dependencies +import fs from "node:fs"; +import pc from "picocolors"; +import { codeFrameColumns } from "@babel/code-frame"; + +const readFileSync = fs.readFileSync; + +// frame has the form "at myMethod (location/to/my/file.js:10:2)" +function getCodeFrame(frame: string) { + const locationStart = frame.indexOf("(") + 1; + const locationEnd = frame.indexOf(")"); + const frameLocation = frame.slice(locationStart, locationEnd); + + const frameLocationElements = frameLocation.split(":"); + const [filename, line, column] = [ + frameLocationElements[0]!, + parseInt(frameLocationElements[1]!, 10), + parseInt(frameLocationElements[2]!, 10), + ]; + + let rawFileContents = ""; + try { + rawFileContents = readFileSync(filename, "utf-8"); + } catch (e) { + return ""; + } + + const codeFrame = codeFrameColumns( + rawFileContents, + { + start: { line, column }, + }, + { + highlightCode: false, + linesBelow: 0, + }, + ); + return `${pc.dim(frameLocation)}\n${codeFrame}\n`; +} + +function getUserCodeFrame() { + // If we couldn't load dependencies, we can't generate the user trace + /* istanbul ignore next */ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!readFileSync || !codeFrameColumns) { + return ""; + } + const err = new Error(); + const firstClientCodeFrame = err.stack + ?.split("\n") + .slice(1) // Remove first line which has the form "Error: TypeError" + .find((frame) => !frame.includes("node_modules/")); // Ignore frames from 3rd party libraries + + return getCodeFrame(firstClientCodeFrame!); +} + +export { getUserCodeFrame }; diff --git a/packages/cli-testing-library/src/helpers.ts b/packages/cli-testing-library/src/helpers.ts new file mode 100644 index 0000000..bc40096 --- /dev/null +++ b/packages/cli-testing-library/src/helpers.ts @@ -0,0 +1,94 @@ +import type { TestInstance } from "./types"; + +function jestFakeTimersAreEnabled() { + /* istanbul ignore else */ + if ( + (typeof vi !== "undefined" && vi.isFakeTimers && vi.isFakeTimers()) || + (typeof jest !== "undefined" && jest !== null) + ) { + return ( + // legacy timers + ( + setTimeout as unknown as { + _isMockFunction: boolean; + } + )._isMockFunction === true || + // modern timers + + Object.prototype.hasOwnProperty.call(setTimeout, "clock") + ); + } + // istanbul ignore next + return false; +} + +const instanceRef = { current: undefined as TestInstance | undefined }; + +if (typeof afterEach === "function") { + afterEach(() => { + instanceRef.current = undefined; + }); +} + +function getCurrentInstance() { + /** + * Worth mentioning that this deviates from the upstream implementation + * of `dom-testing-library`'s `getDocument` in waitFor, which throws an error whenever + * `window` is not defined. + * + * Admittedly, this is another way that `cli-testing-library` will need to figure out + * the right solution to this problem, since there is no omni-present parent `instance` + * in a CLI like there is in a browser. (although FWIW, "process" might work) + * + * Have ideas how to solve? Please let us know: + * https://github.com/crutchcorn/cli-testing-library/issues/ + */ + return instanceRef.current; +} + +// TODO: Does this need to be namespaced for each test that runs? +// That way, we don't end up with a "singleton" that ends up wiped between +// parallel tests. +function setCurrentInstance(newInstance: TestInstance) { + instanceRef.current = newInstance; +} + +function debounce) => void>( + func: T, + timeout: number, +): (...args: Parameters) => void { + let timer: ReturnType; + + return (...args: Parameters) => { + clearTimeout(timer); + timer = setTimeout(() => { + // @ts-ignore this is fine + func.apply(this, args); + }, timeout); + }; +} + +/** + * This is used to bind a series of functions where `instance` is the first argument + * to an instance, removing the implicit first argument. + */ +function bindObjectFnsToInstance( + instance: TestInstance, + object: Record) => unknown>, +) { + return Object.entries(object).reduce( + (prev, [key, fn]) => { + prev[key] = (...props: Array) => fn(instance, ...props); + return prev; + }, + {} as typeof object, + ); +} + +export { + jestFakeTimersAreEnabled, + setCurrentInstance, + getCurrentInstance, + debounce, + bindObjectFnsToInstance, +}; diff --git a/packages/cli-testing-library/src/index.ts b/packages/cli-testing-library/src/index.ts new file mode 100644 index 0000000..b793479 --- /dev/null +++ b/packages/cli-testing-library/src/index.ts @@ -0,0 +1,39 @@ +import { cleanup } from "./pure"; + +// if we're running in a test runner that supports afterEach +// or teardown then we'll automatically run cleanup afterEach test +// this ensures that tests run in isolation from each other +// if you don't like this then set the CTL_SKIP_AUTO_CLEANUP env variable to 'true'. +if ( + typeof process === "undefined" || + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + !(process.env && process.env.CTL_SKIP_AUTO_CLEANUP) +) { + // ignore teardown() in code coverage because Jest does not support it + /* istanbul ignore else */ + if (typeof afterEach === "function") { + afterEach(async () => { + await cleanup(); + }); + } else if (typeof teardown === "function") { + // Block is guarded by `typeof` check. + // eslint does not support `typeof` guards. + + teardown(async () => { + await cleanup(); + }); + } +} + +export * from "./config"; +export * from "./helpers"; +export * from "./events"; +export * from "./get-queries-for-instance"; +export * from "./matches"; +export * from "./pure"; +export * from "./query-helpers"; +export * from "./queries/index"; +export * as queries from "./queries/index"; +export * from "./mutation-observer"; +export * from "./wait-for"; +export * from "./user-event/index"; diff --git a/packages/cli-testing-library/src/jest-globals.ts b/packages/cli-testing-library/src/jest-globals.ts new file mode 100644 index 0000000..2838c1a --- /dev/null +++ b/packages/cli-testing-library/src/jest-globals.ts @@ -0,0 +1,11 @@ +import globals from "@jest/globals"; +import * as extensions from "./matchers/index"; +import type { CLITestingLibraryMatchers } from "./matchers/types"; + +globals.expect.extend(extensions); + +declare module "@jest/expect" { + // eslint-disable-next-line @typescript-eslint/naming-convention + export interface Matchers> + extends CLITestingLibraryMatchers {} +} diff --git a/packages/cli-testing-library/src/jest.ts b/packages/cli-testing-library/src/jest.ts new file mode 100644 index 0000000..dd5f7a5 --- /dev/null +++ b/packages/cli-testing-library/src/jest.ts @@ -0,0 +1,12 @@ +import * as extensions from "./matchers/index"; +import type { CLITestingLibraryMatchers } from "./matchers/types"; + +expect.extend(extensions); + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + // eslint-disable-next-line @typescript-eslint/naming-convention + interface Matchers extends CLITestingLibraryMatchers {} + } +} diff --git a/packages/cli-testing-library/src/matchers/index.ts b/packages/cli-testing-library/src/matchers/index.ts new file mode 100644 index 0000000..c680e79 --- /dev/null +++ b/packages/cli-testing-library/src/matchers/index.ts @@ -0,0 +1,4 @@ +import { toBeInTheConsole } from "./to-be-in-the-console"; +import { toHaveErrorMessage } from "./to-have-errormessage"; + +export { toBeInTheConsole, toHaveErrorMessage }; diff --git a/packages/cli-testing-library/src/matchers/to-be-in-the-console.ts b/packages/cli-testing-library/src/matchers/to-be-in-the-console.ts new file mode 100644 index 0000000..1a84871 --- /dev/null +++ b/packages/cli-testing-library/src/matchers/to-be-in-the-console.ts @@ -0,0 +1,37 @@ +import { getDefaultNormalizer } from "../matches"; +import { checkCliInstance, getMessage } from "./utils"; +import type { TestInstance } from "../types"; + +export function toBeInTheConsole(this: any, instance: TestInstance) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (instance !== null || !this.isNot) { + checkCliInstance(instance, toBeInTheConsole, this); + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const errormessage = instance + ? getDefaultNormalizer()( + instance.stdoutArr.map((obj) => obj.contents).join("\n"), + ) + : null; + + return { + // Does not change based on `.not`, and as a result, we must confirm if it _actually_ is there + pass: !!instance, + message: () => { + const to = this.isNot ? "not to" : "to"; + return getMessage( + this, + this.utils.matcherHint( + `${this.isNot ? ".not" : ""}.toBeInTheConsole`, + "instance", + "", + ), + `Expected ${to} find the instance in the console`, + "", + "Received", + this.utils.printReceived(errormessage), + ); + }, + }; +} diff --git a/packages/cli-testing-library/src/matchers/to-have-errormessage.ts b/packages/cli-testing-library/src/matchers/to-have-errormessage.ts new file mode 100644 index 0000000..212e599 --- /dev/null +++ b/packages/cli-testing-library/src/matchers/to-have-errormessage.ts @@ -0,0 +1,40 @@ +import { getDefaultNormalizer } from "../matches"; +import { checkCliInstance, getMessage } from "./utils"; +import type { TestInstance } from "../types"; + +export function toHaveErrorMessage( + this: any, + testInstance: TestInstance, + checkWith?: string | RegExp, +) { + checkCliInstance(testInstance, toHaveErrorMessage, this); + + const expectsErrorMessage = checkWith !== undefined; + + const errormessage = getDefaultNormalizer()( + testInstance.stderrArr.map((obj) => obj.contents).join("\n"), + ); + + return { + pass: expectsErrorMessage + ? checkWith instanceof RegExp + ? checkWith.test(errormessage) + : this.equals(errormessage, checkWith) + : Boolean(testInstance.stderrArr.length), + message: () => { + const to = this.isNot ? "not to" : "to"; + return getMessage( + this, + this.utils.matcherHint( + `${this.isNot ? ".not" : ""}.toHaveErrorMessage`, + "instance", + "", + ), + `Expected the instance ${to} have error message`, + this.utils.printExpected(checkWith), + "Received", + this.utils.printReceived(errormessage), + ); + }, + }; +} diff --git a/packages/cli-testing-library/src/matchers/types.ts b/packages/cli-testing-library/src/matchers/types.ts new file mode 100644 index 0000000..57fbe2d --- /dev/null +++ b/packages/cli-testing-library/src/matchers/types.ts @@ -0,0 +1,17 @@ +export interface CLITestingLibraryMatchers { + /** + * @description + * Assert whether a query is present in the console or not. + * @example + * expect(queryByText('Hello world')).toBeInTheDocument() + */ + toBeInTheConsole: () => TReturn; + + /** + * @description + * Check whether the given instance has a stderr message or not. + * @example + * expect(instance).toHaveErrorMessage(/command could not be found/i) // to partially match + */ + toHaveErrorMessage: (checkWith?: string | RegExp) => TReturn; +} diff --git a/packages/cli-testing-library/src/matchers/utils.ts b/packages/cli-testing-library/src/matchers/utils.ts new file mode 100644 index 0000000..c9a7f0f --- /dev/null +++ b/packages/cli-testing-library/src/matchers/utils.ts @@ -0,0 +1,93 @@ +import redent from "redent"; +import type { TestInstance } from "../types"; + +class GenericTypeError extends Error { + constructor( + expectedString: string, + received: any, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + matcherFn: Function, + context: any, + ) { + super(); + + /* istanbul ignore next */ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (Error.captureStackTrace) { + Error.captureStackTrace(this, matcherFn); + } + let withType = ""; + try { + withType = context.utils.printWithType( + "Received", + received, + context.utils.printReceived, + ); + } catch (e) { + // Can throw for Document: + // https://github.com/jsdom/jsdom/issues/2304 + } + this.message = [ + context.utils.matcherHint( + `${context.isNot ? ".not" : ""}.${matcherFn.name}`, + "received", + "", + ), + "", + + `${context.utils.RECEIVED_COLOR( + "received", + )} value must ${expectedString}.`, + withType, + ].join("\n"); + } +} + +type GenericTypeErrorArgs = ConstructorParameters; + +type AllButFirst = T extends [infer _First, ...infer Rest] ? Rest : never; + +class CliInstanceTypeError extends GenericTypeError { + constructor(...args: AllButFirst) { + super("be a TestInstance", ...args); + } +} + +type CliInstanceTypeErrorArgs = ConstructorParameters< + typeof CliInstanceTypeError +>; + +function checkCliInstance( + cliInstance: TestInstance, + ...args: AllButFirst +) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!(cliInstance && cliInstance.process && cliInstance.process.stdout)) { + throw new CliInstanceTypeError(cliInstance, ...args); + } +} + +function display(context: any, value: any) { + return typeof value === "string" ? value : context.utils.stringify(value); +} + +function getMessage( + context: any, + matcher: string, + expectedLabel: string, + expectedValue: string, + receivedLabel: string, + receivedValue: string, +) { + return [ + `${matcher}\n`, + `${expectedLabel}:\n${context.utils.EXPECTED_COLOR( + redent(display(context, expectedValue), 2), + )}`, + `${receivedLabel}:\n${context.utils.RECEIVED_COLOR( + redent(display(context, receivedValue), 2), + )}`, + ].join("\n"); +} + +export { CliInstanceTypeError, checkCliInstance, getMessage }; diff --git a/packages/cli-testing-library/src/matches.ts b/packages/cli-testing-library/src/matches.ts new file mode 100644 index 0000000..3007716 --- /dev/null +++ b/packages/cli-testing-library/src/matches.ts @@ -0,0 +1,160 @@ +import stripAnsiFn from "strip-ansi"; +import type { TestInstance } from "./types"; + +export type MatcherFunction = ( + content: string, + element: TestInstance | null, +) => boolean; + +export type Matcher = MatcherFunction | RegExp | number | string; + +export type NormalizerFn = (text: string) => string; + +export interface NormalizerOptions extends DefaultNormalizerOptions { + normalizer?: NormalizerFn; +} + +export interface MatcherOptions { + exact?: boolean; + /** Use normalizer with getDefaultNormalizer instead */ + trim?: boolean; + /** Use normalizer with getDefaultNormalizer instead */ + stripAnsi?: boolean; + /** Use normalizer with getDefaultNormalizer instead */ + collapseWhitespace?: boolean; + normalizer?: NormalizerFn; + /** suppress suggestions for a specific query */ + suggest?: boolean; +} + +export type Match = ( + textToMatch: string, + node: TestInstance | null, + matcher: Matcher, + options?: MatcherOptions, +) => boolean; + +export interface DefaultNormalizerOptions { + trim?: boolean; + collapseWhitespace?: boolean; + stripAnsi?: boolean; +} + +function assertNotNullOrUndefined(matcher: Matcher) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (matcher === null || matcher === undefined) { + throw new Error( + `It looks like ${matcher} was passed instead of a matcher. Did you do something like getByText(${matcher})?`, + ); + } +} + +/** + * @private + */ +function fuzzyMatches( + textToMatch: string, + node: TestInstance | null, + matcher: Matcher, + normalizer: NormalizerFn, +) { + if (typeof textToMatch !== "string") { + return false; + } + assertNotNullOrUndefined(matcher); + + const normalizedText = normalizer(textToMatch); + + if (typeof matcher === "string" || typeof matcher === "number") { + return normalizedText + .toLowerCase() + .includes(matcher.toString().toLowerCase()); + } else if (typeof matcher === "function") { + return matcher(normalizedText, node); + } else { + return matcher.test(normalizedText); + } +} + +/** + * @private + */ +function matches( + textToMatch: string, + node: TestInstance | null, + matcher: Matcher, + normalizer: NormalizerFn, +): boolean { + if (typeof textToMatch !== "string") { + return false; + } + + assertNotNullOrUndefined(matcher); + + const normalizedText = normalizer(textToMatch); + if (matcher instanceof Function) { + return matcher(normalizedText, node); + } else if (matcher instanceof RegExp) { + return matcher.test(normalizedText); + } else { + return normalizedText === String(matcher); + } +} + +function getDefaultNormalizer({ + trim = true, + collapseWhitespace = true, + stripAnsi = true, +}: DefaultNormalizerOptions = {}): NormalizerFn { + return (text: string) => { + let normalizedText = text; + normalizedText = trim ? normalizedText.trim() : normalizedText; + normalizedText = collapseWhitespace + ? normalizedText.replace(/\s+/g, " ") + : normalizedText; + normalizedText = stripAnsi ? stripAnsiFn(normalizedText) : normalizedText; + return normalizedText; + }; +} + +/** + * @param {Object} props + * Constructs a normalizer to pass to functions in matches.js + * @param {boolean|undefined} props.trim The user-specified value for `trim`, without + * any defaulting having been applied + * @param {boolean|undefined} props.stripAnsi The user-specified value for `stripAnsi`, without + * any defaulting having been applied + * @param {boolean|undefined} props.collapseWhitespace The user-specified value for + * `collapseWhitespace`, without any defaulting having been applied + * @param {Function|undefined} props.normalizer The user-specified normalizer + * @returns {Function} A normalizer + */ +function makeNormalizer({ + trim, + stripAnsi, + collapseWhitespace, + normalizer, +}: NormalizerOptions): NormalizerFn { + if (normalizer) { + // User has specified a custom normalizer + if ( + typeof trim !== "undefined" || + typeof collapseWhitespace !== "undefined" || + typeof stripAnsi !== "undefined" + ) { + // They've also specified a value for trim or collapseWhitespace + throw new Error( + "trim and collapseWhitespace are not supported with a normalizer. " + + "If you want to use the default trim and collapseWhitespace logic in your normalizer, " + + 'use "getDefaultNormalizer({trim, collapseWhitespace})" and compose that into your normalizer', + ); + } + + return normalizer; + } else { + // No custom normalizer specified. Just use default. + return getDefaultNormalizer({ trim, collapseWhitespace, stripAnsi }); + } +} + +export { fuzzyMatches, matches, getDefaultNormalizer, makeNormalizer }; diff --git a/src/mutation-observer.js b/packages/cli-testing-library/src/mutation-observer.ts similarity index 58% rename from src/mutation-observer.js rename to packages/cli-testing-library/src/mutation-observer.ts index 5181c80..463d934 100644 --- a/src/mutation-observer.js +++ b/packages/cli-testing-library/src/mutation-observer.ts @@ -1,28 +1,31 @@ // Used for `MutationObserver`. Unsure if it's really needed, but it's worth mentioning that these are not tied to // specific CLI instances of `render`. This means that if there are e2e CLI tests that run in parallel, they will // execute far more frequently than needed. -const _observers = new Map() +const _observers = new Map(); // Not perfect as a way to make "MutationObserver" unique IDs, but it should work -let mutId = 0 +let mutId = 0; class MutationObserver { - constructor(cb) { - this._id = ++mutId - this._cb = cb + _cb: () => void; + _id: number; + + constructor(cb: () => void) { + this._id = ++mutId; + this._cb = cb; } observe() { - _observers.set(this._id, this._cb) + _observers.set(this._id, this._cb); } disconnect() { - _observers.delete(this._id) + _observers.delete(this._id); } } function _runObservers() { - Array.from(_observers.values()).forEach(cb => cb()) + Array.from(_observers.values()).forEach((cb) => cb()); } -export {_runObservers, MutationObserver} +export { _runObservers, MutationObserver }; diff --git a/packages/cli-testing-library/src/pretty-cli.ts b/packages/cli-testing-library/src/pretty-cli.ts new file mode 100644 index 0000000..bb7d310 --- /dev/null +++ b/packages/cli-testing-library/src/pretty-cli.ts @@ -0,0 +1,38 @@ +import sliceAnsi from "slice-ansi"; +import { getUserCodeFrame } from "./get-user-code-frame"; +import type { TestInstance } from "./types"; + +function prettyCLI(testInstance: TestInstance, maxLength?: number) { + if (typeof maxLength !== "number") { + maxLength = + (typeof process !== "undefined" && + Number(process.env.DEBUG_PRINT_LIMIT)) || + 7000; + } + + if (maxLength === 0) { + return ""; + } + + if (!("stdoutArr" in testInstance && "stderrArr" in testInstance)) { + throw new TypeError(`Expected an instance but got ${testInstance}`); + } + + const outStr = testInstance.getStdallStr(); + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return maxLength !== undefined && outStr.length > maxLength + ? sliceAnsi(outStr, 0, maxLength) + : outStr; +} + +const logCLI = (...args: Parameters) => { + const userCodeFrame = getUserCodeFrame(); + if (userCodeFrame) { + process.stdout.write(`${prettyCLI(...args)}\n\n${userCodeFrame}`); + } else { + process.stdout.write(prettyCLI(...args)); + } +}; + +export { prettyCLI, logCLI }; diff --git a/packages/cli-testing-library/src/process-helpers.ts b/packages/cli-testing-library/src/process-helpers.ts new file mode 100644 index 0000000..1342daf --- /dev/null +++ b/packages/cli-testing-library/src/process-helpers.ts @@ -0,0 +1,71 @@ +import treeKill from "tree-kill"; +import { getConfig } from "./config"; +import type { TestInstance } from "./types"; + +export const killProc = (instance: TestInstance, signal: string | undefined) => + new Promise((resolve, reject) => { + if (!instance.process.pid || (instance.process.pid && instance.hasExit())) { + resolve(); + return; + } + + treeKill(instance.process.pid, signal, async (err) => { + try { + if (err) { + if ( + err.message.includes("The process") && + err.message.includes("not found.") + ) { + resolve(); + return; + } + if ( + err.message.includes("could not be terminated") && + err.message.includes("There is no running instance of the task.") && + instance.hasExit() + ) { + resolve(); + return; + } + const isOperationNotSupported = err.message.includes( + "The operation attempted is not supported.", + ); + const isAccessDenied = err.message.includes("Access is denied."); + if ( + err.message.includes("could not be terminated") && + (isOperationNotSupported || isAccessDenied) + ) { + const sleep = (t: number) => new Promise((r) => setTimeout(r, t)); + await sleep(getConfig().errorDebounceTimeout); + if (instance.hasExit()) { + resolve(); + return; + } + console.warn("Ran into error while trying to kill process:"); + console.warn(err.toString()); + console.warn(`This is likely due to Window's permissions. + Because this error is prevalent on CI Windows systems with the tree-kill package, we are attempting + an alternative kill method.`); + console.warn(); + console.warn( + "Be aware that this alternative kill method is not guaranteed to work with subprocesses, and they may not exit properly as a result.", + ); + + const didKill = instance.process.kill(signal as "SIGKILL"); + if (didKill) { + resolve(); + } else { + console.error( + "Alternative kill method failed. Rejecting with original error.", + ); + reject(err); + } + return; + } + reject(err); + } else resolve(); + } catch (e: unknown) { + reject(e); + } + }); + }); diff --git a/packages/cli-testing-library/src/pure.ts b/packages/cli-testing-library/src/pure.ts new file mode 100644 index 0000000..b7959a2 --- /dev/null +++ b/packages/cli-testing-library/src/pure.ts @@ -0,0 +1,179 @@ +import childProcess from "node:child_process"; +import { performance } from "node:perf_hooks"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import stripFinalNewline from "strip-final-newline"; +import { _runObservers } from "./mutation-observer"; +import { getQueriesForElement } from "./get-queries-for-instance"; +import userEvent from "./user-event/index"; +import { bindObjectFnsToInstance, setCurrentInstance } from "./helpers"; +import { fireEvent } from "./events"; +import { getConfig } from "./config"; +import { logCLI } from "./pretty-cli"; +import type { TestInstance } from "./types"; +import type * as queries from "./queries/index"; +import type { SpawnOptionsWithoutStdio } from "node:child_process"; +import type { BoundFunction } from "./get-queries-for-instance"; + +const __curDir = + typeof __dirname === "undefined" + ? // @ts-ignore ESM requires this, but it doesn't work in Node18 + path.dirname(fileURLToPath(import.meta.url)) + : __dirname; + +export interface RenderOptions { + cwd: string; + debug: boolean; + spawnOpts: Omit; +} + +type UserEvent = typeof userEvent; + +export type RenderResult = TestInstance & { + userEvent: { + [P in keyof UserEvent]: BoundFunction; + }; +} & { [P in keyof typeof queries]: BoundFunction<(typeof queries)[P]> }; + +const mountedInstances = new Set(); + +async function render( + command: string, + args: Array = [], + opts: Partial = {}, +): Promise { + const { cwd = __curDir, spawnOpts = {} } = opts; + + const exec = childProcess.spawn(command, args, { + ...spawnOpts, + cwd, + shell: true, + }); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + let _readyPromiseInternals: null | { resolve: Function; reject: Function } = + null; + + let _resolved = false; + + const execOutputAPI = { + __exitCode: null as null | number, + _isOutputAPI: true, + _isReady: new Promise( + (resolve, reject) => (_readyPromiseInternals = { resolve, reject }), + ), + process: exec, + // Clear buffer of stdout to do more accurate `t.regex` checks + clear() { + execOutputAPI.stdoutArr = []; + execOutputAPI.stderrArr = []; + }, + debug(maxLength?: number) { + logCLI(execOutputAPI, maxLength); + }, + // An array of strings gathered from stdout when unable to do + // `await stdout` because of inquirer interactive prompts + stdoutArr: [] as Array<{ contents: Buffer | string; timestamp: number }>, + stderrArr: [] as Array<{ contents: Buffer | string; timestamp: number }>, + hasExit() { + return this.__exitCode === null ? null : { exitCode: this.__exitCode }; + }, + getStdallStr(): string { + return this.stderrArr + .concat(this.stdoutArr) + .sort((a, b) => (a.timestamp < b.timestamp ? -1 : 1)) + .map((obj) => obj.contents) + .join("\n"); + }, + } as TestInstance & { + __exitCode: null | number; + _isOutputAPI: true; + _isReady: Promise; + }; + + mountedInstances.add(execOutputAPI as unknown as TestInstance); + + exec.stdout.on("data", (result: string | Buffer) => { + // `on('spawn') doesn't work the same way in Node12. + // Instead, we have to rely on this working as-expected. + if (_readyPromiseInternals && !_resolved) { + _readyPromiseInternals.resolve(); + _resolved = true; + } + + const resStr = stripFinalNewline(result as string); + execOutputAPI.stdoutArr.push({ + contents: resStr, + timestamp: performance.now(), + }); + _runObservers(); + }); + + exec.stderr.on("data", (result: string | Buffer) => { + if (_readyPromiseInternals && !_resolved) { + _readyPromiseInternals.resolve(); + _resolved = true; + } + + const resStr = stripFinalNewline(result as string); + execOutputAPI.stderrArr.push({ + contents: resStr, + timestamp: performance.now(), + }); + _runObservers(); + }); + + exec.on("error", (result) => { + if (_readyPromiseInternals) { + _readyPromiseInternals.reject(result); + } + }); + + exec.on("spawn", () => { + setTimeout(() => { + if (_readyPromiseInternals && !_resolved) { + _readyPromiseInternals.resolve(); + _resolved = true; + } + }, getConfig().renderAwaitTime); + }); + + exec.on("exit", (code) => { + execOutputAPI.__exitCode = code ?? 0; + }); + + setCurrentInstance(execOutputAPI); + + await execOutputAPI._isReady; + + function getStdallStr(this: Omit) { + return this.stderrArr + .concat(this.stdoutArr) + .sort((a, b) => (a.timestamp < b.timestamp ? -1 : 1)) + .map((obj) => obj.contents) + .join("\n"); + } + + return Object.assign( + execOutputAPI, + { + userEvent: bindObjectFnsToInstance(execOutputAPI, userEvent as never), + getStdallStr: getStdallStr.bind(execOutputAPI), + }, + getQueriesForElement(execOutputAPI), + ) as TestInstance as RenderResult; +} + +function cleanup() { + return Promise.all(Array.from(mountedInstances).map(cleanupAtInstance)); +} + +// maybe one day we'll expose this (perhaps even as a utility returned by render). +// but let's wait until someone asks for it. +async function cleanupAtInstance(instance: TestInstance) { + await fireEvent.sigkill(instance); + + mountedInstances.delete(instance); +} + +export { render, cleanup }; diff --git a/packages/cli-testing-library/src/queries/all-utils.ts b/packages/cli-testing-library/src/queries/all-utils.ts new file mode 100644 index 0000000..882f235 --- /dev/null +++ b/packages/cli-testing-library/src/queries/all-utils.ts @@ -0,0 +1,3 @@ +export * from "../matches"; +export * from "../query-helpers"; +export * from "../config"; diff --git a/packages/cli-testing-library/src/queries/error.ts b/packages/cli-testing-library/src/queries/error.ts new file mode 100644 index 0000000..763e882 --- /dev/null +++ b/packages/cli-testing-library/src/queries/error.ts @@ -0,0 +1,75 @@ +import { + buildQueries, + fuzzyMatches, + makeNormalizer, + matches, +} from "./all-utils"; +import type { + GetErrorFunction, + Matcher, + SelectorMatcherOptions, +} from "./all-utils"; +import type { TestInstance } from "../types"; +import type { waitForOptions } from "../wait-for"; + +export type QueryByError = ( + instance: TestInstance, + id: Matcher, + options?: SelectorMatcherOptions, +) => T | null; + +export type GetByError = ( + instance: TestInstance, + id: Matcher, + options?: SelectorMatcherOptions, +) => T; + +export type FindByError = ( + instance: TestInstance, + id: Matcher, + options?: SelectorMatcherOptions, + waitForElementOptions?: waitForOptions, +) => Promise; + +const queryByErrorBase: QueryByError = ( + instance, + text, + { exact = false, collapseWhitespace, trim, normalizer, stripAnsi } = {}, +) => { + const matcher = exact ? matches : fuzzyMatches; + const matchNormalizer = makeNormalizer({ + stripAnsi, + collapseWhitespace, + trim, + normalizer, + }); + const str = instance.stderrArr.map((obj) => obj.contents).join("\n"); + if (matcher(str, instance, text, matchNormalizer)) return instance; + else return null; +}; + +const getMissingError: GetErrorFunction<[unknown]> = (_c, text) => + `Unable to find an stdout line with the text: ${text}. This could be because the text is broken up by multiple lines. In this case, you can provide a function for your text matcher to make your matcher more flexible.`; + +const [_queryByErrorWithSuggestions, _getByError, _findByError] = buildQueries( + queryByErrorBase, + getMissingError, +); + +export function getByError( + ...args: Parameters> +): ReturnType> { + return _getByError(...args); +} + +export function queryByError( + ...args: Parameters> +): ReturnType> { + return _queryByErrorWithSuggestions(...args); +} + +export function findByError( + ...args: Parameters> +): ReturnType> { + return _findByError(...args); +} diff --git a/packages/cli-testing-library/src/queries/index.ts b/packages/cli-testing-library/src/queries/index.ts new file mode 100644 index 0000000..7df0510 --- /dev/null +++ b/packages/cli-testing-library/src/queries/index.ts @@ -0,0 +1,2 @@ +export * from "./text"; +export * from "./error"; diff --git a/packages/cli-testing-library/src/queries/text.ts b/packages/cli-testing-library/src/queries/text.ts new file mode 100644 index 0000000..56e25f3 --- /dev/null +++ b/packages/cli-testing-library/src/queries/text.ts @@ -0,0 +1,73 @@ +import { + buildQueries, + fuzzyMatches, + makeNormalizer, + matches, +} from "./all-utils"; +import type { TestInstance } from "../types"; +import type { + GetErrorFunction, + Matcher, + SelectorMatcherOptions, +} from "./all-utils"; +import type { waitForOptions } from "../wait-for"; + +export type QueryByText = ( + instance: TestInstance, + id: Matcher, + options?: SelectorMatcherOptions, +) => T | null; + +export type GetByText = ( + instance: TestInstance, + id: Matcher, + options?: SelectorMatcherOptions, +) => T; + +export type FindByText = ( + instance: TestInstance, + id: Matcher, + options?: SelectorMatcherOptions, + waitForElementOptions?: waitForOptions, +) => Promise; + +const queryByTextBase: QueryByText = ( + instance, + text, + { exact = false, collapseWhitespace, trim, normalizer, stripAnsi } = {}, +) => { + const matcher = exact ? matches : fuzzyMatches; + const matchNormalizer = makeNormalizer({ + stripAnsi, + collapseWhitespace, + trim, + normalizer, + }); + const str = instance.stdoutArr.map((output) => output.contents).join("\n"); + if (matcher(str, instance, text, matchNormalizer)) return instance; + else return null; +}; + +const getMissingError: GetErrorFunction<[unknown]> = (_c, text) => + `Unable to find an stdout line with the text: ${text}. This could be because the text is broken up by multiple lines. In this case, you can provide a function for your text matcher to make your matcher more flexible.`; + +const [_queryByTextWithSuggestions, _getByText, _findByText] = buildQueries( + queryByTextBase, + getMissingError, +); + +export function getByText( + ...args: Parameters> +): ReturnType> { + return _getByText(...args); +} +export function queryByText( + ...args: Parameters> +): ReturnType> { + return _queryByTextWithSuggestions(...args); +} +export function findByText( + ...args: Parameters> +): ReturnType> { + return _findByText(...args); +} diff --git a/packages/cli-testing-library/src/query-helpers.ts b/packages/cli-testing-library/src/query-helpers.ts new file mode 100644 index 0000000..b7faa13 --- /dev/null +++ b/packages/cli-testing-library/src/query-helpers.ts @@ -0,0 +1,150 @@ +import { getSuggestedQuery } from "./suggestions"; +import { waitFor } from "./wait-for"; +import { getConfig } from "./config"; +import type { waitForOptions as WaitForOptions } from "./wait-for"; +import type { Variant } from "./suggestions"; +import type { TestInstance } from "./types"; +import type { Matcher, MatcherOptions } from "./matches"; + +export type WithSuggest = { suggest?: boolean }; + +export type GetErrorFunction = [string]> = ( + c: TestInstance | null, + ...args: TArguments +) => string; + +export interface SelectorMatcherOptions extends MatcherOptions { + selector?: string; + ignore?: boolean | string; +} + +export type QueryMethod, TReturn> = ( + container: TestInstance, + ...args: TArguments +) => TReturn; + +function getInstanceError(message: string | null, instance: TestInstance) { + return getConfig().getInstanceError(message, instance); +} + +function getSuggestionError( + suggestion: { toString: () => string }, + container: TestInstance, +) { + return getConfig().getInstanceError( + `A better query is available, try this: +${suggestion.toString()} +`, + container, + ); +} + +// this accepts a query function and returns a function which throws an error +// if an empty list of elements is returned +function makeGetQuery>( + queryBy: (instance: TestInstance, ...args: TArguments) => TestInstance | null, + getMissingError: GetErrorFunction, +) { + return ( + instance: TestInstance, + ...args: TArguments + ): T => { + const el = queryBy(instance, ...args); + if (!el) { + throw getConfig().getInstanceError( + getMissingError(instance, ...args), + instance, + ); + } + + return el as T; + }; +} + +// this accepts a getter query function and returns a function which calls +// waitFor and passing a function which invokes the getter. +function makeFindQuery( + getter: ( + container: TestInstance, + text: Matcher, + options?: MatcherOptions, + ) => TQueryFor, +) { + return ( + instance: TestInstance, + text: Matcher, + options?: MatcherOptions, + waitForOptions?: WaitForOptions, + ): Promise => { + return waitFor( + () => { + return getter(instance, text, options) as unknown as T; + }, + { instance, ...waitForOptions }, + ); + }; +} + +const wrapSingleQueryWithSuggestion = + >( + query: ( + container: TestInstance, + ...args: TArguments + ) => TestInstance | null, + queryByName: string, + variant: Variant, + ) => + ( + container: TestInstance, + ...args: TArguments + ): T => { + const instance = query(container, ...args); + const [{ suggest = getConfig().throwSuggestions } = {}] = args.slice( + -1, + ) as [WithSuggest]; + if (instance && suggest) { + const suggestion = getSuggestedQuery(instance, variant); + if (suggestion && !queryByName.endsWith(suggestion.queryName)) { + throw getSuggestionError(suggestion.toString(), container); + } + } + + return instance as T; + }; + +function buildQueries( + queryBy: QueryMethod< + [matcher: Matcher, options?: MatcherOptions], + TestInstance | null + >, + getMissingError: GetErrorFunction< + [matcher: Matcher, options?: MatcherOptions] + >, +) { + const getBy = makeGetQuery(queryBy, getMissingError); + + const queryByWithSuggestions = wrapSingleQueryWithSuggestion( + queryBy, + queryBy.name, + "get", + ); + + const getByWithSuggestions = wrapSingleQueryWithSuggestion( + getBy, + queryBy.name, + "get", + ); + + const findBy = makeFindQuery( + wrapSingleQueryWithSuggestion(getBy, queryBy.name, "find"), + ); + + return [queryByWithSuggestions, getByWithSuggestions, findBy] as const; +} + +export { + getInstanceError, + wrapSingleQueryWithSuggestion, + makeFindQuery, + buildQueries, +}; diff --git a/packages/cli-testing-library/src/suggestions.ts b/packages/cli-testing-library/src/suggestions.ts new file mode 100644 index 0000000..1ac20d0 --- /dev/null +++ b/packages/cli-testing-library/src/suggestions.ts @@ -0,0 +1,111 @@ +import { getDefaultNormalizer } from "./matches"; +import type { TestInstance } from "./types"; + +export interface QueryOptions { + [key: string]: RegExp | boolean; +} + +export type QueryArgs = [string, QueryOptions?]; + +export interface Suggestion { + queryName: string; + queryMethod: string; + queryArgs: QueryArgs; + variant: string; + warning?: string; + toString: () => string; +} + +export type Variant = "find" | "get" | "query"; + +export type Method = "Text" | "text"; + +const normalize = getDefaultNormalizer(); + +function escapeRegExp(string: string) { + return string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string +} + +function getRegExpMatcher(string: string) { + return new RegExp(escapeRegExp(string.toLowerCase()), "i"); +} + +function makeSuggestion( + queryName: string, + _instance: TestInstance, + content: string, + { + variant, + name, + }: { + variant: Variant; + name?: string; + }, +): Suggestion | undefined { + const warning = "" as string; + const queryOptions = {} as QueryOptions; + const queryArgs = [ + [].includes(queryName as never) ? content : getRegExpMatcher(content), + ] as QueryArgs; + + if (name) { + queryOptions.name = getRegExpMatcher(name); + } + + if (Object.keys(queryOptions).length > 0) { + queryArgs.push(queryOptions); + } + + const queryMethod = `${variant}By${queryName}`; + + return { + queryName, + queryMethod, + queryArgs, + variant, + warning, + toString() { + if (warning) { + console.warn(warning); + } + const [text, options] = queryArgs; + + const newText = typeof text === "string" ? `'${text}'` : text; + + const newOptions = options + ? `, { ${Object.entries(options) + .map(([k, v]) => `${k}: ${v}`) + .join(", ")} }` + : ""; + + return `${queryMethod}(${newText}${newOptions})`; + }, + }; +} + +function canSuggest( + currentMethod: Method, + requestedMethod: Method | undefined, + data: unknown, +) { + return ( + data && + (!requestedMethod || + requestedMethod.toLowerCase() === currentMethod.toLowerCase()) + ); +} + +export function getSuggestedQuery( + instance: TestInstance, + variant: Variant = "get", + method?: Method, +): Suggestion | undefined { + const textContent = normalize( + instance.stdoutArr.map((obj) => obj.contents).join("\n"), + ); + if (canSuggest("Text", method, textContent)) { + return makeSuggestion("Text", instance, textContent, { variant }); + } + + return undefined; +} diff --git a/packages/cli-testing-library/src/types.ts b/packages/cli-testing-library/src/types.ts new file mode 100644 index 0000000..4e21951 --- /dev/null +++ b/packages/cli-testing-library/src/types.ts @@ -0,0 +1,19 @@ +import type { ChildProcessWithoutNullStreams } from "node:child_process"; + +export interface TestInstance { + clear: () => void; + process: ChildProcessWithoutNullStreams; + stdoutArr: Array<{ contents: Buffer | string; timestamp: number }>; + stderrArr: Array<{ contents: Buffer | string; timestamp: number }>; + getStdallStr: () => string; + hasExit: () => null | { exitCode: number }; + debug: (maxLength?: number) => void; +} + +declare global { + const jest: undefined | any; + const vi: undefined | any; + const afterEach: undefined | ((fn: () => void) => void); + const teardown: undefined | ((fn: () => void) => void); + const expect: undefined | any; +} diff --git a/packages/cli-testing-library/src/user-event/index.ts b/packages/cli-testing-library/src/user-event/index.ts new file mode 100644 index 0000000..1604b15 --- /dev/null +++ b/packages/cli-testing-library/src/user-event/index.ts @@ -0,0 +1,9 @@ +import { keyboard } from "./keyboard/index"; + +const userEvent = { + keyboard, +}; + +export default userEvent; + +export type { keyboardKey } from "./keyboard/index"; diff --git a/src/user-event/keyboard/getNextKeyDef.ts b/packages/cli-testing-library/src/user-event/keyboard/getNextKeyDef.ts similarity index 59% rename from src/user-event/keyboard/getNextKeyDef.ts rename to packages/cli-testing-library/src/user-event/keyboard/getNextKeyDef.ts index 5afc61b..298a210 100644 --- a/src/user-event/keyboard/getNextKeyDef.ts +++ b/packages/cli-testing-library/src/user-event/keyboard/getNextKeyDef.ts @@ -1,7 +1,7 @@ -import {keyboardKey, keyboardOptions} from './types' +import type { keyboardKey, keyboardOptions } from "./types"; enum bracketDict { - '[' = ']', + "[" = "]", } /** @@ -15,56 +15,56 @@ export function getNextKeyDef( text: string, options: keyboardOptions, ): { - keyDef: keyboardKey - consumedLength: number + keyDef: keyboardKey; + consumedLength: number; } { - const {type, descriptor, consumedLength} = readNextDescriptor(text) + const { type, descriptor, consumedLength } = readNextDescriptor(text); - const keyDef: keyboardKey = options.keyboardMap.find(def => { - if (type === '[') { - return def.code?.toLowerCase() === descriptor.toLowerCase() + const keyDef: keyboardKey = options.keyboardMap.find((def) => { + if (type === "[") { + return def.code?.toLowerCase() === descriptor.toLowerCase(); } - return def.hex === descriptor + return def.hex === descriptor; }) ?? { code: descriptor, - hex: 'Unknown', - } + hex: "Unknown", + }; return { keyDef, consumedLength, - } + }; } function readNextDescriptor(text: string) { - let pos = 0 + let pos = 0; const startBracket = - text[pos] in bracketDict ? (text[pos] as keyof typeof bracketDict) : '' + text[pos]! in bracketDict ? (text[pos] as keyof typeof bracketDict) : ""; - pos += startBracket.length + pos += startBracket.length; // `foo[[bar` is an escaped char at position 3, // but `foo[[[>5}bar` should be treated as `{` pressed down for 5 keydowns. const startBracketRepeated = startBracket ? (text.match(new RegExp(`^\\${startBracket}+`)) as RegExpMatchArray)[0] .length - : 0 - const isEscapedChar = startBracketRepeated === 2 + : 0; + const isEscapedChar = startBracketRepeated === 2; - const type = isEscapedChar ? '' : startBracket + const type = isEscapedChar ? "" : startBracket; return { type, - ...(type === '' ? readPrintableChar(text, pos) : readTag(text, pos, type)), - } + ...(type === "" ? readPrintableChar(text, pos) : readTag(text, pos, type)), + }; } function readPrintableChar(text: string, pos: number) { - const descriptor = text[pos] + const descriptor = text[pos]; - assertDescriptor(descriptor, text, pos) + assertDescriptor(descriptor, text, pos); - pos += descriptor.length + pos += descriptor.length; return { consumedLength: pos, @@ -72,7 +72,7 @@ function readPrintableChar(text: string, pos: number) { releasePrevious: false, releaseSelf: true, repeat: 1, - } + }; } function readTag( @@ -80,25 +80,27 @@ function readTag( pos: number, startBracket: keyof typeof bracketDict, ) { - const descriptor = text.slice(pos).match(/^\w+/)?.[0] + const descriptor = text.slice(pos).match(/^\w+/)?.[0]; - assertDescriptor(descriptor, text, pos) + assertDescriptor(descriptor, text, pos); - pos += descriptor.length + pos += descriptor.length; - const expectedEndBracket = bracketDict[startBracket] - const endBracket = text[pos] === expectedEndBracket ? expectedEndBracket : '' + const expectedEndBracket = bracketDict[startBracket]; + const endBracket = text[pos] === expectedEndBracket ? expectedEndBracket : ""; if (!endBracket) { - throw new Error(getErrorMessage(`"${expectedEndBracket}"`, text[pos], text)) + throw new Error( + getErrorMessage(`"${expectedEndBracket}"`, text[pos], text), + ); } - pos += endBracket.length + pos += endBracket.length; return { consumedLength: pos, descriptor, - } + }; } function assertDescriptor( @@ -107,7 +109,7 @@ function assertDescriptor( pos: number, ): asserts descriptor is string { if (!descriptor) { - throw new Error(getErrorMessage('key descriptor', text[pos], text)) + throw new Error(getErrorMessage("key descriptor", text[pos], text)); } } @@ -116,7 +118,7 @@ function getErrorMessage( found: string | undefined, text: string, ) { - return `Expected ${expected} but found "${found ?? ''}" in "${text}" + return `Expected ${expected} but found "${found ?? ""}" in "${text}" See https://github.com/testing-library/user-event/blob/main/README.md#keyboardtext-options - for more information about how userEvent parses your input.` + for more information about how userEvent parses your input.`; } diff --git a/packages/cli-testing-library/src/user-event/keyboard/index.ts b/packages/cli-testing-library/src/user-event/keyboard/index.ts new file mode 100644 index 0000000..3ac9e2b --- /dev/null +++ b/packages/cli-testing-library/src/user-event/keyboard/index.ts @@ -0,0 +1,39 @@ +import { keyboardImplementation } from "./keyboardImplementation"; +import { defaultKeyMap } from "./keyMap"; +import type { TestInstance } from "../../types"; +import type { keyboardKey, keyboardOptions } from "./types"; + +export type { keyboardOptions, keyboardKey }; + +export function keyboard( + instance: TestInstance, + text: string, + options?: Partial, +): void | Promise { + const { promise } = keyboardImplementationWrapper(instance, text, options); + + if ((options?.delay ?? 0) > 0) { + return promise; + } else { + // prevent users from dealing with UnhandledPromiseRejectionWarning in sync call + promise.catch(console.error); + } +} + +export function keyboardImplementationWrapper( + instance: TestInstance, + text: string, + config: Partial = {}, +): { + promise: Promise; +} { + const { delay = 0, keyboardMap = defaultKeyMap } = config; + const options = { + delay, + keyboardMap, + }; + + return { + promise: keyboardImplementation(instance, text, options), + }; +} diff --git a/packages/cli-testing-library/src/user-event/keyboard/keyMap.ts b/packages/cli-testing-library/src/user-event/keyboard/keyMap.ts new file mode 100644 index 0000000..8da3112 --- /dev/null +++ b/packages/cli-testing-library/src/user-event/keyboard/keyMap.ts @@ -0,0 +1,114 @@ +import type { keyboardKey } from "./types"; + +/** + * Mapping for a default US-104-QWERTY keyboard + * + * These use ANSI-C quoting, which seems to work for Linux, macOS, and Windows alike + * @see https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#ANSI_002dC-Quoting + * @see https://stackoverflow.com/questions/35429671/detecting-key-press-within-bash-scripts + * @see https://gist.github.com/crutchcorn/2811db78a7b924cf54f4507198427fd2 + */ +export const defaultKeyMap: Array = [ + // alphanumeric keys + { code: "Digit!", hex: "\x21" }, + { code: "Digit#", hex: "\x23" }, + { code: "Digit$", hex: "\x24" }, + { code: "Digit%", hex: "\x25" }, + { code: "Digit&", hex: "\x26" }, + { code: "Digit(", hex: "\x29" }, + { code: "Digit)", hex: "\x29" }, + { code: "Digit*", hex: "\x2a" }, + { code: "Digit-", hex: "\x2d" }, + { code: "Digit@", hex: "\x40" }, + { code: "Digit^", hex: "\x5e" }, + { code: "Digit{", hex: "\x7b" }, + { code: "Digit|", hex: "\x7c" }, + { code: "Digit}", hex: "\x7d" }, + { code: "Digit~", hex: "\x7e" }, + { code: "Digit0", hex: "\x30" }, + { code: "Digit1", hex: "\x31" }, + { code: "Digit2", hex: "\x32" }, + { code: "Digit3", hex: "\x33" }, + { code: "Digit4", hex: "\x34" }, + { code: "Digit5", hex: "\x35" }, + { code: "Digit6", hex: "\x36" }, + { code: "Digit7", hex: "\x37" }, + { code: "Digit8", hex: "\x38" }, + { code: "Digit9", hex: "\x39" }, + { code: "KeyA", hex: "\x41" }, + { code: "KeyB", hex: "\x42" }, + { code: "KeyC", hex: "\x43" }, + { code: "KeyD", hex: "\x44" }, + { code: "KeyE", hex: "\x45" }, + { code: "KeyF", hex: "\x46" }, + { code: "KeyG", hex: "\x47" }, + { code: "KeyH", hex: "\x48" }, + { code: "KeyI", hex: "\x49" }, + { code: "KeyJ", hex: "\x4a" }, + { code: "KeyK", hex: "\x4b" }, + { code: "KeyL", hex: "\x4c" }, + { code: "KeyM", hex: "\x4d" }, + { code: "KeyN", hex: "\x4e" }, + { code: "KeyO", hex: "\x4f" }, + { code: "KeyP", hex: "\x50" }, + { code: "KeyQ", hex: "\x51" }, + { code: "KeyR", hex: "\x52" }, + { code: "KeyS", hex: "\x53" }, + { code: "KeyT", hex: "\x54" }, + { code: "KeyU", hex: "\x55" }, + { code: "KeyV", hex: "\x56" }, + { code: "KeyW", hex: "\x57" }, + { code: "KeyX", hex: "\x58" }, + { code: "KeyY", hex: "\x59" }, + { code: "KeyZ", hex: "\x5a" }, + { code: "Digit_", hex: "\x5f" }, + { code: "KeyLowerA", hex: "\x61" }, + { code: "KeyLowerB", hex: "\x62" }, + { code: "KeyLowerC", hex: "\x63" }, + { code: "KeyLowerD", hex: "\x64" }, + { code: "KeyLowerE", hex: "\x65" }, + { code: "KeyLowerF", hex: "\x66" }, + { code: "KeyLowerG", hex: "\x67" }, + { code: "KeyLowerH", hex: "\x68" }, + { code: "KeyLowerI", hex: "\x69" }, + { code: "KeyLowerJ", hex: "\x6a" }, + { code: "KeyLowerK", hex: "\x6b" }, + { code: "KeyLowerL", hex: "\x6c" }, + { code: "KeyLowerM", hex: "\x6d" }, + { code: "KeyLowerN", hex: "\x6e" }, + { code: "KeyLowerO", hex: "\x6f" }, + { code: "KeyLowerP", hex: "\x70" }, + { code: "KeyLowerQ", hex: "\x71" }, + { code: "KeyLowerR", hex: "\x72" }, + { code: "KeyLowerS", hex: "\x73" }, + { code: "KeyLowerT", hex: "\x74" }, + { code: "KeyLowerU", hex: "\x75" }, + { code: "KeyLowerV", hex: "\x76" }, + { code: "KeyLowerW", hex: "\x77" }, + { code: "KeyLowerX", hex: "\x78" }, + { code: "KeyLowerY", hex: "\x79" }, + { code: "KeyLowerZ", hex: "\x7a" }, + + // alphanumeric block - functional + { code: "Space", hex: "\x20" }, + { code: "Backspace", hex: "\x08" }, + { code: "Enter", hex: "\x0D" }, + + // function + { code: "Escape", hex: "\x1b" }, + + // arrows + { code: "ArrowUp", hex: "\x1b\x5b\x41" }, + { code: "ArrowDown", hex: "\x1B\x5B\x42" }, + { code: "ArrowLeft", hex: "\x1b\x5b\x44" }, + { code: "ArrowRight", hex: "\x1b\x5b\x43" }, + + // control pad + { code: "Home", hex: "\x1b\x4f\x48" }, + { code: "End", hex: "\x1b\x4f\x46" }, + { code: "Delete", hex: "\x1b\x5b\x33\x7e" }, + { code: "PageUp", hex: "\x1b\x5b\x35\x7e" }, + { code: "PageDown", hex: "\x1b\x5b\x36\x7e" }, + + // TODO: add mappings +]; diff --git a/packages/cli-testing-library/src/user-event/keyboard/keyboardImplementation.ts b/packages/cli-testing-library/src/user-event/keyboard/keyboardImplementation.ts new file mode 100644 index 0000000..0666a07 --- /dev/null +++ b/packages/cli-testing-library/src/user-event/keyboard/keyboardImplementation.ts @@ -0,0 +1,32 @@ +import { fireEvent } from "../../events"; +import { wait } from "../utils"; +import { getNextKeyDef } from "./getNextKeyDef"; +import type { TestInstance } from "../../types"; +import type { keyboardKey, keyboardOptions } from "./types"; + +export async function keyboardImplementation( + instance: TestInstance, + text: string, + options: keyboardOptions, +): Promise { + const { keyDef, consumedLength } = getNextKeyDef(text, options); + + keypress(keyDef, instance); + + if (text.length > consumedLength) { + if (options.delay > 0) { + await wait(options.delay); + } + + return keyboardImplementation( + instance, + text.slice(consumedLength), + options, + ); + } + return void undefined; +} + +function keypress(keyDef: keyboardKey, instance: TestInstance) { + fireEvent.write(instance, { value: keyDef.hex! }); +} diff --git a/src/user-event/keyboard/types.ts b/packages/cli-testing-library/src/user-event/keyboard/types.ts similarity index 71% rename from src/user-event/keyboard/types.ts rename to packages/cli-testing-library/src/user-event/keyboard/types.ts index 0ffa27f..c97ca87 100644 --- a/src/user-event/keyboard/types.ts +++ b/packages/cli-testing-library/src/user-event/keyboard/types.ts @@ -1,13 +1,13 @@ export type keyboardOptions = { /** Delay between keystrokes */ - delay: number + delay: number; /** Keyboard layout to use */ - keyboardMap: keyboardKey[] -} + keyboardMap: Array; +}; export interface keyboardKey { /** Physical location on a keyboard */ - code?: string + code?: string; /** Character or functional key hex code */ - hex?: string + hex?: string; } diff --git a/packages/cli-testing-library/src/user-event/utils.ts b/packages/cli-testing-library/src/user-event/utils.ts new file mode 100644 index 0000000..e5d6d0a --- /dev/null +++ b/packages/cli-testing-library/src/user-event/utils.ts @@ -0,0 +1,3 @@ +export function wait(time?: number) { + return new Promise((resolve) => setTimeout(() => resolve(), time)); +} diff --git a/packages/cli-testing-library/src/vitest.ts b/packages/cli-testing-library/src/vitest.ts new file mode 100644 index 0000000..6ca84f8 --- /dev/null +++ b/packages/cli-testing-library/src/vitest.ts @@ -0,0 +1,14 @@ +/* istanbul ignore file */ + +import { expect } from "vitest"; +import * as extensions from "./matchers/index"; +import type { CLITestingLibraryMatchers } from "./matchers/types"; + +expect.extend(extensions); + +declare module "vitest" { + interface Assertion extends CLITestingLibraryMatchers {} + + interface AsymmetricMatchersContaining + extends CLITestingLibraryMatchers {} +} diff --git a/src/wait-for.js b/packages/cli-testing-library/src/wait-for.ts similarity index 50% rename from src/wait-for.js rename to packages/cli-testing-library/src/wait-for.ts index 6ee9d11..4f42e98 100644 --- a/src/wait-for.js +++ b/packages/cli-testing-library/src/wait-for.ts @@ -1,60 +1,79 @@ // Migrated from: https://github.com/testing-library/dom-testing-library/blob/main/src/wait-for.js // TODO: Migrate back to use `config.js` file -import {getCurrentInstance, jestFakeTimersAreEnabled} from './helpers' -import {MutationObserver} from './mutation-observer' -import {getConfig} from './config' +import { getCurrentInstance, jestFakeTimersAreEnabled } from "./helpers"; +import { MutationObserver } from "./mutation-observer"; +import { getConfig } from "./config"; +import type { TestInstance } from "./types"; // This is so the stack trace the developer sees is one that's // closer to their code (because async stack traces are hard to follow). -function copyStackTrace(target, source) { - target.stack = source.stack.replace(source.message, target.message) +function copyStackTrace(target: Error, source: Error) { + target.stack = source.stack!.replace(source.message, target.message); } -function waitFor( - callback, +export interface waitForOptions { + instance?: TestInstance; + showOriginalStackTrace?: boolean; + timeout?: number; + interval?: number; + onTimeout?: (error: Error) => Error; + stackTraceError?: Error; +} + +function waitFor( + callback: () => Promise | T, { instance = getCurrentInstance(), timeout = getConfig().asyncUtilTimeout, showOriginalStackTrace = getConfig().showOriginalStackTrace, stackTraceError, interval = 50, - onTimeout = error => { + onTimeout = (error) => { error.message = getConfig().getInstanceError( error.message, - instance, - ).message - return error + instance!, + ).message; + return error; }, + }: Omit & { + stackTraceError: Error; + } = { + stackTraceError: new Error("STACK_TRACE_MESSAGE"), }, -) { - if (typeof callback !== 'function') { - throw new TypeError('Received `callback` arg must be a function') +): Promise { + if (typeof callback !== "function") { + throw new TypeError("Received `callback` arg must be a function"); } + // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { - let lastError, intervalId, observer - let finished = false - let promiseStatus = 'idle' + let lastError: Error | null = null; + let intervalId!: NodeJS.Timeout; + let observer: MutationObserver; + let finished = false; + let promiseStatus = "idle"; - const overallTimeoutTimer = setTimeout(handleTimeout, timeout) + const overallTimeoutTimer = setTimeout(handleTimeout, timeout); - const usingJestFakeTimers = jestFakeTimersAreEnabled() + const usingJestFakeTimers = jestFakeTimersAreEnabled(); if (usingJestFakeTimers) { - const {unstable_advanceTimersWrapper: advanceTimersWrapper} = getConfig() - checkCallback() + const { unstable_advanceTimersWrapper: advanceTimersWrapper } = + getConfig(); + checkCallback(); // this is a dangerous rule to disable because it could lead to an // infinite loop. However, eslint isn't smart enough to know that we're // setting finished inside `onDone` which will be called when we're done // waiting or when we've timed out. - // eslint-disable-next-line no-unmodified-loop-condition + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition while (!finished) { if (!jestFakeTimersAreEnabled()) { const error = new Error( `Changed from using fake timers to real timers while using waitFor. This is not allowed and will result in very strange behavior. Please ensure you're awaiting all async things your test is doing before changing to real timers. For more info, please go to https://github.com/testing-library/dom-testing-library/issues/830`, - ) - if (!showOriginalStackTrace) copyStackTrace(error, stackTraceError) - reject(error) - return + ); + if (!showOriginalStackTrace) copyStackTrace(error, stackTraceError); + reject(error); + return; } // we *could* (maybe should?) use `advanceTimersToNextTimer` but it's // possible that could make this loop go on forever if someone is using @@ -62,47 +81,54 @@ function waitFor( // the user's timer's don't get a chance to resolve. So we'll advance // by an interval instead. (We have a test for this case). advanceTimersWrapper(() => { - jest.advanceTimersByTime(interval) - }) + if (typeof jest !== "undefined") { + jest.advanceTimersByTime(interval); + } else if (typeof vi !== "undefined") { + vi.advanceTimersByTime(interval); + } + }); // It's really important that checkCallback is run *before* we flush // in-flight promises. To be honest, I'm not sure why, and I can't quite // think of a way to reproduce the problem in a test, but I spent // an entire day banging my head against a wall on this. - checkCallback() + checkCallback(); // In this rare case, we *need* to wait for in-flight promises // to resolve before continuing. We don't need to take advantage // of parallelization so we're fine. // https://stackoverflow.com/a/59243586/971592 - // eslint-disable-next-line no-await-in-loop + await advanceTimersWrapper(async () => { - await new Promise(r => { - setTimeout(r, 0) - jest.advanceTimersByTime(0) - }) - }) + await new Promise((r) => { + setTimeout(r, 0); + if (typeof jest !== "undefined") jest.advanceTimersByTime(0); + else if (typeof vi !== "undefined") vi.advanceTimersByTime(0); + }); + }); } } else { - intervalId = setInterval(checkRealTimersCallback, interval) - observer = new MutationObserver(checkRealTimersCallback) - observer.observe() - checkCallback() + intervalId = setInterval(checkRealTimersCallback, interval); + observer = new MutationObserver(checkRealTimersCallback); + observer.observe(); + checkCallback(); } - function onDone(error, result) { - finished = true - clearTimeout(overallTimeoutTimer) + function onDone(error: null, result: T): void; + function onDone(error: Error, result: null): void; + function onDone(error: Error | null, result: T | null) { + finished = true; + clearTimeout(overallTimeoutTimer); if (!usingJestFakeTimers) { - clearInterval(intervalId) - observer.disconnect() + clearInterval(intervalId); + observer.disconnect(); } if (error) { - reject(error) + reject(error); } else { - resolve(result) + resolve(result!); } } @@ -110,71 +136,71 @@ function waitFor( if (jestFakeTimersAreEnabled()) { const error = new Error( `Changed from using real timers to fake timers while using waitFor. This is not allowed and will result in very strange behavior. Please ensure you're awaiting all async things your test is doing before changing to fake timers. For more info, please go to https://github.com/testing-library/dom-testing-library/issues/830`, - ) - if (!showOriginalStackTrace) copyStackTrace(error, stackTraceError) - return reject(error) + ); + if (!showOriginalStackTrace) copyStackTrace(error, stackTraceError); + return reject(error); } else { - return checkCallback() + return checkCallback(); } } function checkCallback() { - if (promiseStatus === 'pending') return + if (promiseStatus === "pending") return; try { - const result = callback() // runWithExpensiveErrorDiagnosticsDisabled(callback) - if (typeof (result && result.then) === 'function') { - promiseStatus = 'pending' + const result = callback(); // runWithExpensiveErrorDiagnosticsDisabled(callback) + const isPromise = (r: unknown): r is Promise => + !!r && typeof (r as Promise).then === "function"; + if (isPromise(result)) { + promiseStatus = "pending"; result.then( - resolvedValue => { - promiseStatus = 'resolved' - onDone(null, resolvedValue) + (resolvedValue) => { + promiseStatus = "resolved"; + onDone(null, resolvedValue); }, - rejectedValue => { - promiseStatus = 'rejected' - lastError = rejectedValue + (rejectedValue) => { + promiseStatus = "rejected"; + lastError = rejectedValue; }, - ) + ); } else { - onDone(null, result) + onDone(null, result); } // If `callback` throws, wait for the next mutation, interval, or timeout. } catch (error) { // Save the most recent callback error to reject the promise with it in the event of a timeout - lastError = error + lastError = error as Error; } } function handleTimeout() { - let error + let error; if (lastError) { - error = lastError + error = lastError; if ( !showOriginalStackTrace && - error.name === 'TestingLibraryElementError' + error.name === "TestingLibraryElementError" ) { - copyStackTrace(error, stackTraceError) + copyStackTrace(error, stackTraceError); } } else { - error = new Error('Timed out in waitFor.') + error = new Error("Timed out in waitFor."); if (!showOriginalStackTrace) { - copyStackTrace(error, stackTraceError) + copyStackTrace(error, stackTraceError); } } - onDone(onTimeout(error), null) + onDone(onTimeout(error), null); } - }) + }); } -function waitForWrapper(callback, options) { +function waitForWrapper( + callback: () => Promise | T, + options?: waitForOptions, +): Promise { // create the error here so its stack trace is as close to the // calling code as possible - const stackTraceError = new Error('STACK_TRACE_MESSAGE') - return waitFor(callback, {stackTraceError, ...options}) + const stackTraceError = new Error("STACK_TRACE_MESSAGE"); + return waitFor(callback, { stackTraceError, ...options }); } -export {waitForWrapper as waitFor} - -/* -eslint - max-lines-per-function: ["error", {"max": 200}], -*/ +export { waitForWrapper as waitFor }; diff --git a/packages/cli-testing-library/tests/events.spec.ts b/packages/cli-testing-library/tests/events.spec.ts new file mode 100644 index 0000000..f73cf1c --- /dev/null +++ b/packages/cli-testing-library/tests/events.spec.ts @@ -0,0 +1,120 @@ +import { resolve } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { cleanup, render } from "../src/pure"; +import { fireEvent } from "../src/events"; +import { waitFor } from "../src/wait-for"; +import userEvent from "../src/user-event"; + +afterEach(async () => { + await cleanup(); +}); + +test("fireEvent write works", async () => { + const props = await render("node", [ + resolve(__dirname, "./execute-scripts/stdio-inquirer.js"), + ]); + + const { clear, findByText } = props; + + const instance = await findByText("First option"); + + expect(instance).toBeTruthy(); + + // Windows uses ">", Linux/MacOS use "❯" + expect(await findByText(/[❯>] One/)).toBeTruthy(); + + clear(); + + const down = "\x1B\x5B\x42"; + fireEvent(instance, "write", { value: down }); + + expect(await findByText(/[❯>] Two/)).toBeTruthy(); + + clear(); + + const enter = "\x0D"; + fireEvent(instance, "write", { value: enter }); + + expect(await findByText("First option: Two")).toBeTruthy(); +}); + +test("FireEvent SigTerm works", async () => { + const { findByText } = await render("node", [ + resolve(__dirname, "./execute-scripts/stdio-inquirer.js"), + ]); + + const instance = await findByText("First option"); + + expect(instance).toBeTruthy(); + + await fireEvent.sigterm(instance); + + await waitFor(() => expect(instance.hasExit()).toBeTruthy()); +}); + +test("FireEvent SigKill works", async () => { + const { findByText } = await render("node", [ + resolve(__dirname, "./execute-scripts/stdio-inquirer.js"), + ]); + + const instance = await findByText("First option"); + + expect(instance).toBeTruthy(); + + await fireEvent.sigkill(instance); + + await waitFor(() => expect(instance.hasExit()).toBeTruthy()); +}); + +test("userEvent basic keyboard works", async () => { + const { findByText } = await render("node", [ + resolve(__dirname, "./execute-scripts/stdio-inquirer-input.js"), + ]); + + const instance = await findByText("What is your name?"); + expect(instance).toBeTruthy(); + + userEvent.keyboard(instance, "Test"); + + expect(await findByText("Test")).toBeTruthy(); +}); + +test("userEvent basic keyboard works when bound", async () => { + const { findByText, userEvent: userEventLocal } = await render("node", [ + resolve(__dirname, "./execute-scripts/stdio-inquirer-input.js"), + ]); + + const instance = await findByText("What is your name?"); + expect(instance).toBeTruthy(); + + userEventLocal.keyboard("Test"); + + expect(await findByText("Test")).toBeTruthy(); +}); + +test("UserEvent.keyboard enter key works", async () => { + const props = await render("node", [ + resolve(__dirname, "./execute-scripts/stdio-inquirer.js"), + ]); + + const { clear, findByText, userEvent: userEventLocal } = props; + + const instance = await findByText("First option"); + + expect(instance).toBeTruthy(); + + // Windows uses ">", Linux/MacOS use "❯" + expect(await findByText(/[❯>] One/)).toBeTruthy(); + + clear(); + + userEventLocal.keyboard("[ArrowDown]"); + + expect(await findByText(/[❯>] Two/)).toBeTruthy(); + + clear(); + + userEventLocal.keyboard("[Enter]"); + + expect(await findByText("First option: Two")).toBeTruthy(); +}); diff --git a/packages/cli-testing-library/tests/execute-scripts/list-args.js b/packages/cli-testing-library/tests/execute-scripts/list-args.js new file mode 100644 index 0000000..ab74fd7 --- /dev/null +++ b/packages/cli-testing-library/tests/execute-scripts/list-args.js @@ -0,0 +1 @@ +console.log(process.argv); diff --git a/packages/cli-testing-library/tests/execute-scripts/log-err.js b/packages/cli-testing-library/tests/execute-scripts/log-err.js new file mode 100644 index 0000000..4fe2bf0 --- /dev/null +++ b/packages/cli-testing-library/tests/execute-scripts/log-err.js @@ -0,0 +1,3 @@ +console.log("Log here"); +console.warn("Warn here"); +console.error("Error here"); diff --git a/packages/cli-testing-library/tests/execute-scripts/log-output.js b/packages/cli-testing-library/tests/execute-scripts/log-output.js new file mode 100644 index 0000000..3ea1ae5 --- /dev/null +++ b/packages/cli-testing-library/tests/execute-scripts/log-output.js @@ -0,0 +1,5 @@ +import pc from "picocolors"; + +console.log("__disable_ansi_serialization"); + +console.log(pc.blue("Hello") + " World" + pc.red("!")); diff --git a/packages/cli-testing-library/tests/execute-scripts/stdio-inquirer-input.js b/packages/cli-testing-library/tests/execute-scripts/stdio-inquirer-input.js new file mode 100644 index 0000000..1eb9806 --- /dev/null +++ b/packages/cli-testing-library/tests/execute-scripts/stdio-inquirer-input.js @@ -0,0 +1,9 @@ +import inquirer from "inquirer"; + +inquirer.prompt([ + { + type: "input", + name: "name", + message: "What is your name?", + }, +]); diff --git a/packages/cli-testing-library/tests/execute-scripts/stdio-inquirer.js b/packages/cli-testing-library/tests/execute-scripts/stdio-inquirer.js new file mode 100644 index 0000000..0d61a1a --- /dev/null +++ b/packages/cli-testing-library/tests/execute-scripts/stdio-inquirer.js @@ -0,0 +1,10 @@ +import inquirer from "inquirer"; + +inquirer.prompt([ + { + type: "list", + name: "value", + message: "First option:", + choices: ["One", "Two", "Three"], + }, +]); diff --git a/packages/cli-testing-library/tests/execute-scripts/throw.js b/packages/cli-testing-library/tests/execute-scripts/throw.js new file mode 100644 index 0000000..dfe1354 --- /dev/null +++ b/packages/cli-testing-library/tests/execute-scripts/throw.js @@ -0,0 +1 @@ +throw new Error("Search for this error in stderr"); diff --git a/packages/cli-testing-library/tests/get-user-code-frame.spec.ts b/packages/cli-testing-library/tests/get-user-code-frame.spec.ts new file mode 100644 index 0000000..b67139f --- /dev/null +++ b/packages/cli-testing-library/tests/get-user-code-frame.spec.ts @@ -0,0 +1,88 @@ +import fs from "node:fs"; +import { afterEach, beforeEach, expect, test, vi } from "vitest"; +import { getUserCodeFrame } from "../src/get-user-code-frame"; + +vi.mock(import("node:fs"), async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + default: { + ...actual.default, + readFileSync: vi.fn( + () => ` + import {screen} from '@testing-library/dom' + it('renders', () => { + document.body.appendChild( + document.createTextNode('Hello world') + ) + screen.debug() + expect(screen.getByText('Hello world')).toBeInTheDocument() + }) + `, + ), + }, + } as never; +}); + +const userStackFrame = + "at somethingWrong (/sample-error/error-example.js:7:14)"; + +let globalErrorMock!: ReturnType; + +beforeEach(() => { + globalErrorMock = vi.spyOn(global, "Error") as never; +}); + +afterEach(() => { + vi.mocked(global.Error).mockRestore(); +}); + +test("it returns only user code frame when code frames from node_modules are first", () => { + const stack = `Error: Kaboom + at Object. (/sample-error/node_modules/@es2050/console/build/index.js:4:10) + ${userStackFrame} + `; + globalErrorMock.mockImplementationOnce(() => ({ stack })); + const userTrace = getUserCodeFrame(); + + expect(userTrace).toMatchInlineSnapshot(` + "/sample-error/error-example.js:7:14 + 5 | document.createTextNode('Hello world') + 6 | ) + > 7 | screen.debug() + | ^ + " + `); +}); + +test("it returns only user code frame when node code frames are present afterwards", () => { + const stack = `Error: Kaboom + at Object. (/sample-error/node_modules/@es2050/console/build/index.js:4:10) + ${userStackFrame} + at Object. (/sample-error/error-example.js:14:1) + at internal/main/run_main_module.js:17:47 + `; + globalErrorMock.mockImplementationOnce(() => ({ stack })); + const userTrace = getUserCodeFrame(); + + expect(userTrace).toMatchInlineSnapshot(` + "/sample-error/error-example.js:7:14 + 5 | document.createTextNode('Hello world') + 6 | ) + > 7 | screen.debug() + | ^ + " + `); +}); + +test("it returns empty string if file from code frame can't be read", () => { + (fs.readFileSync as ReturnType).mockImplementationOnce(() => { + throw Error(); + }); + const stack = `Error: Kaboom + ${userStackFrame} + `; + globalErrorMock.mockImplementationOnce(() => ({ stack })); + + expect(getUserCodeFrame()).toEqual(""); +}); diff --git a/packages/cli-testing-library/tests/matchers.spec.ts b/packages/cli-testing-library/tests/matchers.spec.ts new file mode 100644 index 0000000..a6278f3 --- /dev/null +++ b/packages/cli-testing-library/tests/matchers.spec.ts @@ -0,0 +1,91 @@ +import { resolve } from "node:path"; +import { expect, test } from "vitest"; +import { render } from "../src/pure"; + +test("toBeInTheConsole should pass when something is in console", async () => { + const { findByText } = await render("node", [ + resolve(__dirname, "./execute-scripts/list-args.js"), + "--version", + ]); + + await expect( + (async () => expect(await findByText("--version")).toBeInTheConsole())(), + ).resolves.not.toThrow(); +}); + +test("toBeInTheConsole should fail when something is not console", async () => { + const { queryByText } = await render("node", [ + resolve(__dirname, "./execute-scripts/list-args.js"), + "--version", + ]); + + expect(() => expect(queryByText("NotHere")).toBeInTheConsole()).toThrow( + /value must be a TestInstance/, + ); +}); + +test("not.toBeInTheConsole should pass something is not console", async () => { + const { queryByText } = await render("node", [ + resolve(__dirname, "./execute-scripts/list-args.js"), + "--version", + ]); + + expect(() => + expect(queryByText("NotHere")).not.toBeInTheConsole(), + ).not.toThrow(); +}); + +test("not.toBeInTheConsole should fail something is console", async () => { + const { queryByText } = await render("node", [ + resolve(__dirname, "./execute-scripts/list-args.js"), + "--version", + ]); + + expect(() => expect(queryByText("--version")).not.toBeInTheConsole()).toThrow( + /Expected not to find the instance in the console/, + ); +}); + +test("toHaveErrorMessage should pass during stderr when no string passed", async () => { + const instance = await render("node", [ + resolve(__dirname, "./execute-scripts/throw.js"), + ]); + + await expect( + (async () => expect(instance).toHaveErrorMessage())(), + ).resolves.not.toThrow(); +}); + +test("toHaveErrorMessage should pass during stderr when string passed", async () => { + const instance = await render("node", [ + resolve(__dirname, "./execute-scripts/throw.js"), + ]); + + await expect( + (async () => + expect(instance).toHaveErrorMessage(/Search for this error in stderr/))(), + ).resolves.not.toThrow(); +}); + +test("toHaveErrorMessage should fail when something is not in stderr", async () => { + const { findByText } = await render("node", [ + resolve(__dirname, "./execute-scripts/list-args.js"), + "--version", + ]); + + const instance = await findByText("--version"); + expect(() => expect(instance).toHaveErrorMessage("Error isn't here")).toThrow( + /Expected the instance to have error message/, + ); +}); + +test("toHaveErrorMessage should fail when null is passed", async () => { + const { queryByText } = await render("node", [ + resolve(__dirname, "./execute-scripts/list-args.js"), + "--version", + ]); + + expect(() => expect(queryByText("NotHere")).toHaveErrorMessage()).toThrow( + /value must be a TestInstance/, + ); +}); diff --git a/packages/cli-testing-library/tests/matches.spec.ts b/packages/cli-testing-library/tests/matches.spec.ts new file mode 100644 index 0000000..ca9cfe5 --- /dev/null +++ b/packages/cli-testing-library/tests/matches.spec.ts @@ -0,0 +1,44 @@ +import { expect, test } from "vitest"; +import { fuzzyMatches, matches } from "../src/matches"; + +// unit tests for text match utils + +const node = null; +const normalizer = (str: string) => str; + +test("matchers accept strings", () => { + expect(matches("ABC", node, "ABC", normalizer)).toBe(true); + expect(fuzzyMatches("ABC", node, "ABC", normalizer)).toBe(true); +}); + +test("matchers accept regex", () => { + expect(matches("ABC", node, /ABC/, normalizer)).toBe(true); + expect(fuzzyMatches("ABC", node, /ABC/, normalizer)).toBe(true); +}); + +test("matchers accept functions", () => { + expect( + matches("ABC", node, (text: string) => text === "ABC", normalizer), + ).toBe(true); + expect( + fuzzyMatches("ABC", node, (text: string) => text === "ABC", normalizer), + ).toBe(true); +}); + +test("matchers return false if text to match is not a string", () => { + expect(matches(null as never, node, "ABC", normalizer)).toBe(false); + expect(fuzzyMatches(null as never, node, "ABC", normalizer)).toBe(false); +}); + +test("matchers throw on invalid matcher inputs", () => { + expect(() => + matches("ABC", node, null as never, normalizer), + ).toThrowErrorMatchingInlineSnapshot( + `[Error: It looks like null was passed instead of a matcher. Did you do something like getByText(null)?]`, + ); + expect(() => + fuzzyMatches("ABC", node, undefined as never, normalizer), + ).toThrowErrorMatchingInlineSnapshot( + `[Error: It looks like undefined was passed instead of a matcher. Did you do something like getByText(undefined)?]`, + ); +}); diff --git a/packages/cli-testing-library/tests/pretty-cli.spec.ts b/packages/cli-testing-library/tests/pretty-cli.spec.ts new file mode 100644 index 0000000..74ec22f --- /dev/null +++ b/packages/cli-testing-library/tests/pretty-cli.spec.ts @@ -0,0 +1,44 @@ +import { resolve } from "node:path"; +import { expect, test } from "vitest"; +import { render } from "../src/pure"; +import { prettyCLI } from "../src/pretty-cli"; + +test("Should pretty print with ANSI codes properly", async () => { + const instance = await render("node", [ + resolve(__dirname, "./execute-scripts/log-output.js"), + ]); + + await instance.findByText("Hello"); + + expect(prettyCLI(instance, 9000)).toMatchInlineSnapshot(` + "__disable_ansi_serialization + Hello World!" + `); +}); + +test("Should escape ANSI codes properly when sliced too thin", async () => { + const instance = await render("node", [ + resolve(__dirname, "./execute-scripts/log-output.js"), + ]); + + await instance.findByText("Hello"); + + expect(prettyCLI(instance, 30)).toMatchInlineSnapshot(` + "__disable_ansi_serialization + H" + `); +}); + +test("Should show proper stderr and stdout output", async () => { + const instance = await render("node", [ + resolve(__dirname, "./execute-scripts/log-err.js"), + ]); + + await instance.findByError("Error here"); + + expect(prettyCLI(instance, 300)).toMatchInlineSnapshot(` + "Log here + Warn here + Error here" + `); +}); diff --git a/packages/cli-testing-library/tests/queries.spec.ts b/packages/cli-testing-library/tests/queries.spec.ts new file mode 100644 index 0000000..3bdfe12 --- /dev/null +++ b/packages/cli-testing-library/tests/queries.spec.ts @@ -0,0 +1,69 @@ +import { resolve } from "node:path"; +import { expect, test } from "vitest"; +import { render } from "../src/pure"; +import { waitFor } from "../src/wait-for"; + +test("findByError should show stderr", async () => { + const { findByError } = await render("node", [ + resolve(__dirname, "./execute-scripts/throw.js"), + ]); + expect(findByError("Search for this error in stderr")).toBeTruthy(); +}); + +test("findByText should find stdout", async () => { + const { findByText } = await render("node", [ + resolve(__dirname, "./execute-scripts/list-args.js"), + "--version", + ]); + + expect(await findByText("--version")).toBeTruthy(); +}); + +test("findByText should throw errors", async () => { + const { findByText } = await render("node", [ + resolve(__dirname, "./execute-scripts/list-args.js"), + "--version", + ]); + + await expect(() => findByText("--nothing")).rejects.toThrow( + "Unable to find an stdout line with the text:", + ); +}); + +test("queryByText should find text", async () => { + const { queryByText } = await render("node", [ + resolve(__dirname, "./execute-scripts/list-args.js"), + "--version", + ]); + + expect(queryByText("--version")).toBeTruthy(); +}); + +test("queryByText should not throw errors", async () => { + const { queryByText } = await render("node", [ + resolve(__dirname, "./execute-scripts/list-args.js"), + "--version", + ]); + + expect(await queryByText("--nothing")).toBeFalsy(); +}); + +test("getByText should find text", async () => { + const { getByText } = await render("node", [ + resolve(__dirname, "./execute-scripts/list-args.js"), + "--version", + ]); + + expect(await waitFor(() => getByText("--version"))).toBeTruthy(); +}); + +test("getByText should throw errors", async () => { + const { getByText } = await render("node", [ + resolve(__dirname, "./execute-scripts/list-args.js"), + "--version", + ]); + + await expect(() => waitFor(() => getByText("--nothing"))).rejects.toThrow( + "Unable to find an stdout line with the text:", + ); +}); diff --git a/packages/cli-testing-library/tests/render-basics.spec.ts b/packages/cli-testing-library/tests/render-basics.spec.ts new file mode 100644 index 0000000..59f3bba --- /dev/null +++ b/packages/cli-testing-library/tests/render-basics.spec.ts @@ -0,0 +1,49 @@ +import { resolve } from "node:path"; +import { expect, test } from "vitest"; +import { render } from "../src/pure"; +import { waitFor } from "../src/wait-for"; + +test("Should expect error codes when intended", async () => { + const instance = await render("node", [ + resolve(__dirname, "./execute-scripts/throw.js"), + ]); + await waitFor(() => + expect(instance.hasExit()).toMatchObject({ exitCode: 1 }), + ); +}); + +test("Should handle argument passing", async () => { + const { findByText } = await render("node", [ + resolve(__dirname, "./execute-scripts/list-args.js"), + "--version", + ]); + + expect(await findByText("--version")).toBeTruthy(); +}); + +test("Is able to make terminal input and view in-progress stdout", async () => { + const props = await render("node", [ + resolve(__dirname, "./execute-scripts/stdio-inquirer.js"), + ]); + + const { clear, findByText, userEvent } = props; + + const instance = await findByText("First option"); + + expect(instance).toBeTruthy(); + + // Windows uses ">", Linux/MacOS use "❯" + expect(await findByText(/[❯>] One/)).toBeTruthy(); + + clear(); + + userEvent.keyboard("[ArrowDown]"); + + expect(await findByText(/[❯>] Two/)).toBeTruthy(); + + clear(); + + userEvent.keyboard("[Enter]"); + + expect(await findByText("First option: Two")).toBeTruthy(); +}); diff --git a/packages/cli-testing-library/tests/setup.ts b/packages/cli-testing-library/tests/setup.ts new file mode 100644 index 0000000..170aef2 --- /dev/null +++ b/packages/cli-testing-library/tests/setup.ts @@ -0,0 +1 @@ +import "../src/vitest"; diff --git a/packages/cli-testing-library/tests/user-keyboard.spec.ts b/packages/cli-testing-library/tests/user-keyboard.spec.ts new file mode 100644 index 0000000..5160319 --- /dev/null +++ b/packages/cli-testing-library/tests/user-keyboard.spec.ts @@ -0,0 +1,19 @@ +import { resolve } from "node:path"; +import { expect, test } from "vitest"; +import { render } from "../src/pure"; +import { fireEvent } from "../src/events"; + +test("Should render { and } in user keyboard", async () => { + const { findByText, userEvent: userEventLocal } = await render("node", [ + resolve(__dirname, "./execute-scripts/stdio-inquirer-input.js"), + ]); + + const instance = await findByText("What is your name?"); + expect(instance).toBeTruthy(); + + userEventLocal.keyboard("{Hello}"); + + expect(await findByText("{Hello}")).toBeTruthy(); + + await fireEvent.sigterm(instance); +}); diff --git a/packages/cli-testing-library/tests/wait-for.spec.ts b/packages/cli-testing-library/tests/wait-for.spec.ts new file mode 100644 index 0000000..db967d2 --- /dev/null +++ b/packages/cli-testing-library/tests/wait-for.spec.ts @@ -0,0 +1,247 @@ +import { expect, test, vi } from "vitest"; +import { waitFor } from "../src/index"; +import { configure, getConfig } from "../src/config"; +// import {render} from '../pure' + +function deferred() { + let resolve!: (...props: Array) => void; + let reject!: (...props: Array) => void; + const promise = new Promise((res, rej) => { + resolve = res as never; + reject = rej as never; + }); + return { promise, resolve, reject }; +} + +test("waits callback to not throw an error", async () => { + const spy = vi.fn(); + // we are using random timeout here to simulate a real-time example + // of an async operation calling a callback at a non-deterministic time + const randomTimeout = Math.floor(Math.random() * 60); + setTimeout(spy, randomTimeout); + + await waitFor(() => expect(spy).toHaveBeenCalledTimes(1)); + expect(spy).toHaveBeenCalledWith(); +}); + +// we used to have a limitation where we had to set an interval of 0 to 1 +// otherwise there would be problems. I don't think this limitation exists +// anymore, but we'll keep this test around to make sure a problem doesn't +// crop up. +test("can accept an interval of 0", () => waitFor(() => {}, { interval: 0 })); + +test("can timeout after the given timeout time", async () => { + const error = new Error("throws every time"); + const result = await waitFor( + () => { + throw error; + }, + { timeout: 8, interval: 5 }, + ).catch((e) => e); + expect(result).toBe(error); +}); + +test("if no error is thrown then throws a timeout error", async () => { + const result = await waitFor( + () => { + throw undefined; + }, + { timeout: 8, interval: 5, onTimeout: (e) => e }, + ).catch((e) => e); + expect(result).toMatchInlineSnapshot(`[Error: Timed out in waitFor.]`); +}); + +test("if showOriginalStackTrace on a timeout error then the stack trace does not include this file", async () => { + const result = await waitFor( + () => { + throw undefined; + }, + { timeout: 8, interval: 5, showOriginalStackTrace: true }, + ).catch((e) => e); + expect(result.stack).not.toMatch(__dirname); +}); + +test("uses full stack error trace when showOriginalStackTrace present", async () => { + const error = new Error("Throws the full stack trace"); + // even if the error is a TestingLibraryElementError + error.name = "TestingLibraryElementError"; + const originalStackTrace = error.stack; + const result = await waitFor( + () => { + throw error; + }, + { timeout: 8, interval: 5, showOriginalStackTrace: true }, + ).catch((e) => e); + expect(result.stack).toBe(originalStackTrace); +}); + +test("does not change the stack trace if the thrown error is not a TestingLibraryElementError", async () => { + const error = new Error("Throws the full stack trace"); + const originalStackTrace = error.stack; + const result = await waitFor( + () => { + throw error; + }, + { timeout: 8, interval: 5 }, + ).catch((e) => e); + expect(result.stack).toBe(originalStackTrace); +}); + +test("provides an improved stack trace if the thrown error is a TestingLibraryElementError", async () => { + const error = new Error("Throws the full stack trace"); + error.name = "TestingLibraryElementError"; + const originalStackTrace = error.stack; + const result = await waitFor( + () => { + throw error; + }, + { timeout: 8, interval: 5 }, + ).catch((e) => e); + // too hard to test that the stack trace is what we want it to be + // so we'll just make sure that it's not the same as the original + expect(result.stack).not.toBe(originalStackTrace); +}); + +test("throws nice error if provided callback is not a function", () => { + const someElement = "Hello"; + expect(() => waitFor(someElement as never)).toThrow( + "Received `callback` arg must be a function", + ); +}); + +// test('timeout logs a pretty DOM', async () => { +// renderIntoDocument(`
how pretty
`) +// const error = await waitFor( +// () => { +// throw new Error('always throws') +// }, +// {timeout: 1}, +// ).catch(e => e) +// expect(error.message).toMatchInlineSnapshot(` +// always throws +// +// Ignored nodes: comments,