diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 4d89058..a9c9571 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,19 +1,42 @@ # Development Documentation +## Quick reference + +| Task | Command | +| ----------------------------- | ---------------------------- | +| Run tests | `npm test` or `npm run test` | +| Tests + coverage | `npm run test-coverage` | +| Lint | `npm run lint` | +| Lint and fix | `npm run lint-fix` | +| Build | `npm run build` | +| Run from source | `npm run dev` | +| Link package | `npm link` (see Build) | +| Run knip (for unused imports) | `npm run knip` | + ## Testing and Code Coverage -### Running Tests +### Run our test suite -The project uses Vitest for testing. To run the test suite you can use: +The project uses Vitest to write and run tests. To run the test suite use the +command: ```bash npm test ``` -This runs all tests with code coverage analysis enabled. +That runs all tests without a coverage report. If you want coverage (handy for +seeing what's covered and what's now), use `npm run test-coverage`—see Coverage +reports below. + +If you like running tests from VS Code, install the Vitest extension or use the +built-in test runner so your tests are discovered and you can debug in the IDE. + +When you run the test suite it will default to running all tests and we continue +to run in "watch" mode. This means that if you make changes to the code, the +tests will automatically re-run. -Note: if you want to run your tests locally in VS Code using the interactive -IDE, be sure to install the Jest (orta) extension so your tests are discovered. +To exit the watch mode, you can press `Ctrl+C` or `Command+C` depending on your +operating system. Or hit `q` which prompt vitest to exit. ### Test Configuration @@ -33,226 +56,189 @@ configuration file: Tests will fail if coverage drops below these thresholds. +TODO: We likely want much higher coverage than this given the use of the project +but for now we are slowly refactoring and improving coverage as we go. + ### Coverage Reports -Run `test-coverage` to generate coverage reports in the `coverage/` directory: +Run `npm run test-coverage` to generate coverage reports in the `coverage/` +directory: -- **`coverage/lcov-report/index.html`** — Interactive HTML report (open in - browser) +- **`coverage/lcov-report/index.html`** — Interactive HTML report (open in a + browser to explore) - **`coverage/lcov.info`** — LCOV format (used by Codecov) -The HTML report provides a visual breakdown of which files are covered by tests. +The HTML report gives you a visual breakdown of which files are covered by +tests. ### Codecov Integration The project uses Codecov to track code coverage over time and on pull requests. -#### CI Integration +#### CI integration -- The GitHub Actions workflow (`.github/workflows/test-deploy.yml`) - automatically uploads coverage to Codecov after running tests +- The GitHub Actions workflow (`.github/workflows/test-deploy.yml`) uploads + coverage to Codecov after running tests - Coverage reports appear as comments on pull requests (if enabled) - The workflow uses `codecov/codecov-action@vxxx` (whatever version is most recent) to upload the `lcov.info` file #### Configuration -Codecov behavior is configured in `.codecov.yml`: +Codecov is configured in `.codecov.yml`: - Patch coverage is tracked (checks coverage of changed code in PRs) - Project-level status checks are disabled - PR comments are disabled - Codecov now requires a token for all uploads so we have one generated in the - repo as a secret `secrets.CODECOV_TOKEN`. + repo as a secret `secrets.CODECOV_TOKEN`. This token was generated by + `@lwasser` but it can be easily regenerated if a maintianer needs to do that. -#### Local Usage +#### Local usage -You don't need a Codecov account to view coverage locally—just run `pnpm test` -and open the HTML report. Codecov integration is primarily for tracking coverage -trends and to simplify PR reviews in the CI/CD pipeline. +You don't need a Codecov account to view coverage locally. Just run +`npm run test-coverage` and open the HTML report. The Codecov integration is +mainly for tracking coverage over time and for PR reviews in CI. ## Linting -2026 update: The project is now using ESLint 9 and the flat config format. As we -update things from the old config we will update this document to reflect the -changes. Once the code base is stable and things work, we can delete some of the -historical documentation. But for now this document can help us track design -decisions and changes made. - -### Overview - -The project uses a native ESLint configuration that was migrated from -kcd-scripts. The setup supports both CommonJS source files and ES module test -files, using ESLint 9's flat config format. - -### What changed - -Previously, the project used `kcd-scripts` for linting configuration. Because -that project is no longer actively maintained, we migrated to a native ESLint -setup that replicates the same behavior. - -**Migration details:** - -- Replaced `kcd-scripts lint` with native ESLint -- Created `eslint.config.mjs` using ESLint 9 flat config format -- Extracted rules from `eslint-config-kentcdodds` to create a simplified, - maintainable config. This included removing a lot of the custom rules and - leaning more on the recommended rules. -- Preserved some custom rule overrides from the original setup -- Supports both CommonJS (source files) and ES modules (test files) - -Note: We should consider refacting the project to use ES modules instead of -CommonJS (if everyone supports that) in the future for consistency. +The project uses ESLint 9 with a native configuration (we migrated from +kcd-scripts). The setup supports both CommonJS source files and ES module test +files. ### How to run the linter -To check for linting errors you can run: +To check for linting errors: ```bash npm run lint ``` -ESlint can automatically fix some errors. to fix errors automatically run: +To fix many issues automatically (recommended before committing): ```bash -npm run lint -- --fix +npm run lint-fix ``` -The `--fix` flag will automatically fix many common issues like: +The fix command handles a lot of common issues, for example: -- Vitest method aliases (e.g., `toThrowError()` → `toThrow()`) +- Vitest method aliases (e.g. `toThrowError()` → `toThrow()`) - Unused eslint-disable directives -- Some formatting issues +- Many formatting issues -**Note:** Some errors require manual fixes (e.g., unused variables, tests -without assertions). +Some things still need manual fixes (e.g. unused variables, tests without +assertions), but lint-fix will get you most of the way there. ### Pre-commit hooks -The project uses Husky to run pre-commit hooks that automatically lint and fix -staged files before each commit. +We use Husky so that staged files are linted and fixed before each commit. That +way the codebase stays consistent without you having to remember to run lint +every time. -#### How it works: +1. When you run `git commit`, Husky runs `lint-staged`. +2. `lint-staged` runs `eslint --fix` and Prettier on staged JS/TS files. +3. If everything passes, the commit goes through; if not, the commit is blocked + so you can fix the issues. -1. When you run `git commit`, Husky intercepts the commit -2. The pre-commit hook runs `lint-staged` -3. `lint-staged` runs `eslint --fix` on all staged JavaScript/TypeScript files -4. If linting passes, the commit proceeds; if it fails, the commit is blocked +If the hook fails, run `npm run lint-fix` (see above) and address any remaining +errors. If you're in a pinch, you can skip the hook once with +`git commit --no-verify`—but if you do that, please mention it in your PR so a +maintainer can help you get things passing. -What this means... this workflow ensures that code is automatically linted and -fixed before it's committed to version control. +Husky and `lint-staged` are configured in `package.json`; the ESLint config +lives in `eslint.config.mjs`. -If you are encountering issues with the pre-commit hook, you can run the -following command to manually lint and fix the files: +### Dependencies + +These are the main packages we use for linting: + +- **eslint** — ESLint core +- **@eslint/js** — Recommended rules +- **eslint-plugin-import** — Import/export rules and module resolution +- **@vitest/eslint-plugin** — Vitest-specific rules for test files +- **globals** — Global variables for Node and Vitest environments + +### Configuration file + +The ESLint config is in `eslint.config.mjs` at the project root. It uses ESLint +9's flat config format and includes: + +- Base rules from `eslint:recommended` +- Import plugin rules +- Vitest plugin rules (for test files only) +- Custom overrides for project-specific needs +- Support for both CommonJS and ES module syntax + +## Build and local testing + +The build has been migrated from `kcd-scripts build` to native Babel. After a +build, the CLI runs from the `dist/` directory. + +### How to build + +From the repo root: ```bash -npm run lint -- --fix +npm run build ``` -Or if it's really problematic, you can skip verification and commit anyway with -the `--no-verify` flag. +That compiles `src/` into `dist/` using Babel (see `babel.config.js`). The entry +point is `dist/cli.js`; you can also run it with `npm start`. + +### Running from source (no build) + +When you're developing, you often don't need to build. You can run the CLI from +source instead: ```bash -git commit --no-verify +npm run dev ``` -If you do this please ping one of the maintainers in the PR that you open so -they can help you fix the issues! +That runs `./src/cli.js` directly. Use it when you're iterating; use the built +package when you want to test the same code path that users get after they +install from npm. -**Configuration:** +### Linking the package locally -- Husky configuration is in `package.json` under the `husky.hooks.pre-commit` - field -- `lint-staged` configuration is in `package.json` under the `lint-staged` field -- The setup uses the native ESLint configuration (`eslint.config.mjs`) +If you want to install the cli as if you'd installed it from npm you can do so +by linking the package locally. -**Note:** Previously, the project used `kcd-scripts pre-commit` which handled -both hook management and linting. We migrated to using `lint-staged` directly -with our native ESLint config to avoid version conflicts. +1. In the CLI repo root, build and link the project: + ```bash + npm run build + npm link + ``` +2. In another directory (any repo that has a `.all-contributorsrc`, or wherever + you want to run the binary), the global `all-contributors` command will now + use your built package. run: -### Dependencies + ```bash + all-contributors add code + all-contributors generate + ``` -The following packages are typical starters for linting a JavaScript project: +3. When you're done, you can unlink with `npm unlink -g all-contributors-cli` in + the CLI repo, and reinstall the real package elsewhere if you need to. -- **eslint** — ESLint core -- **@eslint/js** — ESLint recommended rules configuration -- **eslint-plugin-import** — Import/export rules for module resolution and best - practices -- **@vitest/eslint-plugin** — Vitest-specific rules for test files -- **globals** — Provides properly formatted global variables for Node.js and - Vitest environments +### Testing the linked package -### Configuration file +Once you've run `npm link`, switch to another directory and run +`all-contributors` commands as you normally would. Confirm that behavior matches +what you expect. -The ESLint configuration is in `eslint.config.mjs` at the project root. It uses -ESLint 9's flat config format and includes: +Remember: after you change code in the CLI repo, run `npm run build` again +before testing from the other directory. The link points at the built `dist/` +output, so your latest changes won't show up until you rebuild. -- Base rules from `eslint:recommended` -- Import plugin rules -- Vitest plugin rules (for test files only) -- Custom overrides for project-specific needs -- Support for both CommonJS and ES module syntax +This workflow is a great way to double-check the built artifact and the exact +code path users get when they install the package. + +### Build dependencies (migration notes) + +We removed most of the build dependencies that kcd-scripts used; they're +redundant with current Node. + +Removed: `@babel/plugin-transform-class-properties`, `babel-plugin-macros`, +`semver`, `@babel/runtime`. -## Build system and process migration - -Our build system has been migrated from `kcd-scripts build` to native Babel. -Below are the dependencies we add for the new native build and a description of -what each does. - -For now we are keeping _some_ of the dependencies from the kcd-scripts build -system. We might be able to remove others as we refine the build process and -better get to know the project. - -### Build dependencies (from kcd-scripts → native Babel) - -- **@babel/core** - - **Status:** Required - - **Purpose:** The Babel compiler. Performs the actual transpilation (parse → - transform → generate). - -- **@babel/cli** - - **Status:** Required - - **Purpose:** Command-line interface. Runs `babel src --out-dir dist` and - feeds files to @babel/core. - -- **@babel/preset-env** - - **Status:** Required - - **Purpose:** Preset that compiles modern JS to match a target environment - (e.g. Node 22). Handles syntax and, optionally, module format. - -- **@babel/plugin-transform-runtime** - - **Status:** REMOVED - - **Purpose:** Replaces inlined Babel helpers with - `require('@babel/runtime/...')` so helpers live in one place. Keeps dist - smaller and avoids duplicating helper code in every file. - -- **@babel/plugin-transform-modules-commonjs** - - **Status:** REMOVED - - **Purpose:** Converts ES module syntax to CommonJS. Only needed if - preset-env is set with `modules: false`; if preset-env uses - `modules: 'commonjs'`, this plugin is redundant. - -- **@babel/plugin-transform-class-properties** - - **Status:** REMOVED - - **Purpose:** Transpiles class properties (including static) in loose mode. - -- **babel-plugin-macros** - - **Status:** Removed - - **Purpose:** Enables macro-based transforms (e.g. preval, codegen). - kcd-scripts includes it by default. We don't use macros in this project. - -- **semver** - - **Status:** Removed - - **Purpose:** Used in a config helper to read `engines.node` from - package.json and derive the target Node version. We hardcode our target - (e.g. `node: '22.22.0'`) in babel.config.js instead. - -- **@babel/runtime** - - **Status:** Required - - **Purpose:** Already a production dependency. Provides the helper functions - that `@babel/plugin-transform-runtime` injects imports for. Required at - runtime when using that plugin. - -IMPORTANT: there are still 2 bugs in the cli that i don't want to fix in this PR -but the cli is broken. We can fix these bugs in a future pr and also add tests -that will catch these bugs in the future. +Kept: `@babel/cli`, `@babel/core` (and any others you see in `package.json`). diff --git a/src/cli.js b/src/cli.js index 359226a..4a11b71 100755 --- a/src/cli.js +++ b/src/cli.js @@ -70,14 +70,14 @@ function startGeneration(argv) { } async function addContribution(argv) { - // ensure the config file exists - await util.configFile.readConfig(argv.config) + const configData = await util.configFile.readConfig(argv.config) + Object.assign(argv, configData) const username = argv._[1] === undefined ? undefined : String(argv._[1]) const contributions = argv._[2] // Add or update contributor in the config file - const data = updateContributors(argv, username, contributions) + const data = await updateContributors(argv, username, contributions) argv.contributors = data.contributors @@ -189,4 +189,8 @@ async function run() { } } -run() +if (require.main === module) { + run() +} else { + module.exports = {addContribution} +} diff --git a/src/contributors/__tests__/add.js b/src/contributors/__tests__/add.js index 84ee3c9..4b79835 100644 --- a/src/contributors/__tests__/add.js +++ b/src/contributors/__tests__/add.js @@ -1,7 +1,14 @@ -import {test, expect} from 'vitest' +import {test, expect, vi} from 'vitest' import addContributor from '../add.js' import fixtures from './fixtures/index.js' +/** + * Mock function that simulates fetching contributor info from an API. + * Returns a promise resolving to contributor data without making real API calls. + * + * @param {string} username - GitHub username to fetch + * @returns {Promise} Contributor info (login, name, avatar_url, profile) + */ function mockInfoFetcher(username) { return Promise.resolve({ login: username, @@ -11,6 +18,12 @@ function mockInfoFetcher(username) { }) } +/** + * Returns test options with one contributor that has a capitalized login ('Login1'). + * Used for testing that case-insensitive username matching works correctly. + * + * @returns {Object} Options object with contributors array containing one entry + */ function caseFixtures() { const options = { contributors: [ @@ -26,6 +39,10 @@ function caseFixtures() { return {options} } +/** + * Tests that addContributor properly propagates errors when infoFetcher rejects. + * The same error should be returned without being swallowed or transformed. + */ test('callback with error if infoFetcher fails', async () => { const {options} = fixtures() const username = 'login3' @@ -44,6 +61,37 @@ test('callback with error if infoFetcher fails', async () => { expect(resolvedError).toBe(error) }) +/** + * Tests that addContributor calls infoFetcher with the correct arguments + * (username, repoType, and repoHost) when adding a new contributor. + * This ensures repository metadata is properly passed for API URL construction. + */ +test('calls infoFetcher with (username, options.repoType, options.repoHost) when adding new contributor', async () => { + const {options} = fixtures() + options.repoType = 'github' + options.repoHost = 'https://github.com' + const username = 'newuser' + const contributions = ['doc'] + const infoFetcher = vi.fn().mockResolvedValue({ + login: username, + name: 'New User', + avatar_url: '', + profile: '', + }) + + await addContributor(options, username, contributions, infoFetcher) + + expect(infoFetcher).toHaveBeenCalledWith( + username, + 'github', + 'https://github.com', + ) +}) + +/** + * Tests that a new contributor is added to the end of the existing contributors list + * with all required fields (login, name, avatar_url, profile, contributions). + */ test('add new contributor at the end of the list of contributors', () => { const {options} = fixtures() const username = 'login3' @@ -63,6 +111,10 @@ test('add new contributor at the end of the list of contributors', () => { ) }) +/** + * Tests that contributions are stored as objects with type and url properties + * when options.url is provided, allowing contributions to link to specific resources. + */ test('add new contributor at the end of the list of contributors with a url link', () => { const {options} = fixtures() const username = 'login3' @@ -83,6 +135,10 @@ test('add new contributor at the end of the list of contributors with a url link ) }) +/** + * Tests that addContributor responds accordingly when a contributor already has + * the exact same contribution types. The contributors list should remain unchanged. + */ test(`should not update an existing contributor's contributions where nothing has changed`, () => { const {options} = fixtures() const username = 'login2' @@ -95,6 +151,10 @@ test(`should not update an existing contributor's contributions where nothing ha ) }) +/** + * Tests that username matching is case-insensitive. Adding contributions for 'login1' + * should match an existing 'Login1' contributor without creating duplicates. + */ test(`should not update an existing contributor's contributions where nothing has changed but the casing`, () => { const {options} = caseFixtures() const username = 'login1' @@ -107,6 +167,10 @@ test(`should not update an existing contributor's contributions where nothing ha ) }) +/** + * Tests that adding a new contribution type to an existing contributor + * updates their contributions array while keeping the list length unchanged. + */ test(`should update an existing contributor's contributions if a new type is added`, () => { const {options} = fixtures() const username = 'login1' @@ -125,6 +189,10 @@ test(`should update an existing contributor's contributions if a new type is add ) }) +/** + * Tests that case-insensitive matching works when updating contributions. + * Adding 'bug' for 'login1' should update 'Login1' while preserving the original casing. + */ test(`should update an existing contributor's contributions if a new type is added with different username case`, () => { const {options} = caseFixtures() const username = 'login1' @@ -143,6 +211,10 @@ test(`should update an existing contributor's contributions if a new type is add ) }) +/** + * Tests that when adding a new contribution type with options.url set, + * the new contribution is stored as an object with type and url properties. + */ test(`should update an existing contributor's contributions if a new type is added with a link`, () => { const {options} = fixtures() const username = 'login1' @@ -163,6 +235,10 @@ test(`should update an existing contributor's contributions if a new type is add ) }) +/** + * Tests that passing a subset of a contributor's current contributions + * replaces (not merges with) their contribution list, allowing removal of types. + */ test(`should update an existing contributor's contributions if an existing type is removed`, () => { const {options} = fixtures() const username = 'login2' diff --git a/src/contributors/__tests__/prompt.js b/src/contributors/__tests__/prompt.js index eab98ed..a1795ad 100644 --- a/src/contributors/__tests__/prompt.js +++ b/src/contributors/__tests__/prompt.js @@ -1,8 +1,9 @@ import {test, expect} from 'vitest' -import prompt from '../prompt.js' +import prompt, {getQuestions} from '../prompt.js' function fixtures() { const options = { + repoType: 'github', contributors: [ { login: 'jfmengels', @@ -58,3 +59,91 @@ test(`should filter valid contribution types from user inserted types`, () => { expect(answers.contributions).toEqual(['code', 'bug']) }) }) + +test(`should prompt for contributions when username is provided but contributions are undefined`, () => { + const options = fixtures() + const username = 'userName' + const contributions = undefined + + const questions = getQuestions(options, username, contributions) + const contributionsQuestion = questions.find(q => q.name === 'contributions') + + expect(contributionsQuestion).toBeDefined() + expect(contributionsQuestion.message).toBe('What are the contribution types?') +}) + +test(`should return prompt with username message when username and contributions are not provided`, () => { + const options = fixtures() + const username = undefined + const contributions = undefined + const questions = getQuestions(options, username, contributions) + const usernameQuestion = questions.find(q => q.name === 'username') + expect(usernameQuestion).toBeDefined() + expect(usernameQuestion.message).toBe( + "Oops. Missing something. What is the contributor's GitHub username?", + ) +}) + +test(`username validation should return error when input is empty`, () => { + const options = fixtures() + const questions = getQuestions(options, undefined, undefined) + const usernameQuestion = questions.find(q => q.name === 'username') + + const result = usernameQuestion.validate('') + expect(result).toBe('Username not provided') +}) + +test(`username validation should return true when input is provided`, () => { + const options = fixtures() + const questions = getQuestions(options, undefined, undefined) + const usernameQuestion = questions.find(q => q.name === 'username') + + const result = usernameQuestion.validate('lwasser') + expect(result).toBe(true) +}) + +test(`contributions validation should return error when no contributions selected`, () => { + const options = fixtures() + const questions = getQuestions(options, 'userName', undefined) + const contributionsQuestion = questions.find(q => q.name === 'contributions') + + // Simulate user selecting nothing (empty array) + const result = contributionsQuestion.validate([], {username: 'userName'}) + expect(result).toBe('Use space to select at least one contribution type.') +}) + +test(`contributions validation should return error when selection matches previous contributions`, () => { + const options = { + repoType: 'github', + contributors: [ + { + login: 'jfmengels', + name: 'Jeroen Engels', + contributions: ['code', 'doc'], // Existing contributions + }, + ], + } + + const questions = getQuestions(options, 'jfmengels', undefined) + const contributionsQuestion = questions.find(q => q.name === 'contributions') + + // The user's contrib types already existing in the all-contribs file + const result = contributionsQuestion.validate(['code', 'doc'], { + username: 'jfmengels', + }) + expect(result).toBe( + 'Nothing changed, use space to select contribution types.', + ) +}) + +test(`contributions validation should return true when valid selection is made`, () => { + const options = fixtures() + const questions = getQuestions(options, 'userName', undefined) + const contributionsQuestion = questions.find(q => q.name === 'contributions') + + // Simulate user selecting valid contributions + const result = contributionsQuestion.validate(['code', 'doc'], { + username: 'userName', + }) + expect(result).toBe(true) +}) diff --git a/src/contributors/prompt.js b/src/contributors/prompt.js index 58fdd90..a5a63e4 100644 --- a/src/contributors/prompt.js +++ b/src/contributors/prompt.js @@ -22,7 +22,7 @@ function getQuestions(options, username, contributions) { { type: 'input', name: 'username', - message: `What is the contributor's ${repo.getTypeName( + message: `Oops. Missing something. What is the contributor's ${repo.getTypeName( options.repoType, )} username?`, when: !username, @@ -101,10 +101,15 @@ module.exports = function prompt(options, username, contributions) { const defaults = { username, contributions: - username === undefined && contributions === undefined + contributions === undefined ? [] : getValidUserContributions(options, contributions), } const questions = getQuestions(options, username, contributions) return inquirer.prompt(questions).then(_.assign(defaults)) } + +/** @testonly */ +module.exports.getQuestions = getQuestions +/** @testonly */ +module.exports.getValidUserContributions = getValidUserContributions diff --git a/src/generate/__tests__/index.js b/src/generate/__tests__/index.js index a6a6a63..3454c43 100644 --- a/src/generate/__tests__/index.js +++ b/src/generate/__tests__/index.js @@ -2,6 +2,13 @@ import {test, expect} from 'vitest' import generate from '../index.js' import contributors from './fixtures/contributors.json' +/** + * Returns test fixtures including options, sample contributor, and markdown content. + * The content includes ALL-CONTRIBUTORS-LIST comment tags where the contributor + * table should be injected. + * + * @returns {Object} Test data with options, jfmengels contributor, and sample content + */ function fixtures() { const options = { projectOwner: 'kentcdodds', @@ -35,6 +42,22 @@ function fixtures() { return {options, jfmengels, content} } +/** + * Tests that generate throws an error when contributors is undefined. + * This prevents callers from forgetting to await the contributors promise + * before passing it to generate. + */ +test('throws when contributors is undefined (caller must await and pass resolved array)', () => { + const {options, content} = fixtures() + expect(() => generate(options, undefined, content)).toThrow( + /Cannot read properties of undefined \(reading 'length'\)/, + ) +}) + +/** + * Tests that generate replaces content between ALL-CONTRIBUTORS-LIST tags + * with a properly formatted table of contributors. + */ test('replace the content between the ALL-CONTRIBUTORS-LIST tags by a table of contributors', () => { const {kentcdodds, bogas04} = contributors const {options, jfmengels, content} = fixtures() @@ -44,6 +67,10 @@ test('replace the content between the ALL-CONTRIBUTORS-LIST tags by a table of c expect(result).toMatchSnapshot() }) +/** + * Tests that generate includes a link to usage documentation when + * linkToUsage option is set to true. + */ test('replace the content between the ALL-CONTRIBUTORS-LIST tags by a table of contributors with linkToUsage', () => { const {kentcdodds, bogas04} = contributors const {options, jfmengels, content} = fixtures() @@ -57,6 +84,10 @@ test('replace the content between the ALL-CONTRIBUTORS-LIST tags by a table of c expect(result).toMatchSnapshot() }) +/** + * Tests that generate does not include a usage link when linkToUsage + * option is explicitly set to false. + */ test('replace the content between the ALL-CONTRIBUTORS-LIST tags by a table of contributors without linkToUsage', () => { const {kentcdodds, bogas04} = contributors const {options, jfmengels, content} = fixtures() @@ -70,6 +101,11 @@ test('replace the content between the ALL-CONTRIBUTORS-LIST tags by a table of c expect(result).toMatchSnapshot() }) +/** + * Tests that generate uses a custom wrapper template when provided. + * The wrapper template can contain <%= bodyContent %> to position the + * contributor list within custom HTML/markdown. + */ test('replace the content between the ALL-CONTRIBUTORS-LIST tags by a custom wrapper around the list of contributors contained in the "bodyContent" tag', () => { const {kentcdodds, bogas04} = contributors const {options, jfmengels, content} = fixtures() @@ -83,6 +119,10 @@ test('replace the content between the ALL-CONTRIBUTORS-LIST tags by a custom wra expect(result).toMatchSnapshot() }) +/** + * Tests that generate splits contributors into multiple table rows when + * the number of contributors exceeds the contributorsPerLine setting. + */ test('split contributors into multiples lines when there are too many', () => { const {kentcdodds} = contributors const {options, content} = fixtures() @@ -100,6 +140,10 @@ test('split contributors into multiples lines when there are too many', () => { expect(result).toMatchSnapshot() }) +/** + * Tests that generate splits contributors into multiple rows with linkToUsage + * enabled when there are more contributors than fit on one line. + */ test('split contributors into multiples lines when there are too many with linkToUsage', () => { const {kentcdodds} = contributors const {options, content} = fixtures() @@ -121,6 +165,11 @@ test('split contributors into multiples lines when there are too many with linkT expect(result).toMatchSnapshot() }) +/** + * Tests that generate sorts contributors alphabetically when + * contributorsSortAlphabetically option is true. The output should + * match regardless of the input order. + */ test('sorts the list of contributors if contributorsSortAlphabetically=true', () => { const {kentcdodds, bogas04} = contributors const {options, jfmengels, content} = fixtures() @@ -141,6 +190,10 @@ test('sorts the list of contributors if contributorsSortAlphabetically=true', () expect(resultPreSorted).toEqual(resultAutoSorted) }) +/** + * Tests that generate returns the content unchanged when there are no + * ALL-CONTRIBUTORS-LIST tags present in the content. + */ test('not inject anything if there is no tags to inject content in', () => { const {kentcdodds} = contributors const {options} = fixtures() @@ -153,6 +206,10 @@ test('not inject anything if there is no tags to inject content in', () => { expect(result).toBe(content) }) +/** + * Tests that generate returns the content unchanged when the start tag + * is malformed (e.g., ALL-CONTRIBUTORS-LIST:SSSSSSSTART instead of :START). + */ test('not inject anything if start tag is malformed', () => { const {kentcdodds} = contributors const {options} = fixtures() @@ -171,6 +228,10 @@ test('not inject anything if start tag is malformed', () => { expect(result).toBe(content) }) +/** + * Tests that generate returns the content unchanged when the end tag + * is malformed (e.g., ALL-CONTRIBUTORS-LIST:EEEEEEEND instead of :END). + */ test('not inject anything if end tag is malformed', () => { const {kentcdodds} = contributors const {options} = fixtures() @@ -189,6 +250,10 @@ test('not inject anything if end tag is malformed', () => { expect(result).toBe(content) }) +/** + * Tests that generate injects only the comment structure (with prettier + * and markdownlint directives) when the contributors list is empty. + */ test('inject nothing if there are no contributors', () => { const {options, content} = fixtures() const contributorList = [] @@ -215,6 +280,11 @@ test('inject nothing if there are no contributors', () => { expect(result).toBe(expected) }) +/** + * Tests that generate updates the all-contributors badge count when + * ALL-CONTRIBUTORS-BADGE tags are present in the content. The badge + * count should match the number of contributors. + */ test('replace all-contributors badge if present', () => { const {kentcdodds} = contributors const {options} = fixtures() @@ -253,6 +323,11 @@ test('replace all-contributors badge if present', () => { expect(result).toBe(expected) }) +/** + * Tests that generate correctly calculates cell width when contributorsPerLine + * results in a decimal value. The width should be floored to an integer to + * ensure valid HTML table attributes. + */ test('validate if cell width attribute is floored correctly', () => { const {kentcdodds} = contributors const {options, content} = fixtures()