Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

`react-component-test-suite` — consistent, reusable test suites for React components in Jest or Vitest, naming each `describe` block after the component under test. Published to npm, bundled by tsdown.

- **Layout**: `src/component-suite.tsx` is the entrypoint, `src/utils.ts` its helpers; unit tests live in `src/__tests__/` with shared fixtures in `src/__tests__/__helpers__/`.
- **Layout**: `src/component-suite.tsx` is the entrypoint, `src/utils/index.ts` its helpers; unit tests live in `src/__tests__/` with shared fixtures in `src/__tests__/__helpers__/`.
- **Peers**: `react`, `@types/react`, and the Testing Library packages are peers; `vitest` and `jest` are optional peers, so nothing may import either test runner at module scope.

## Code conventions
Expand All @@ -22,4 +22,5 @@ Conventions live outside this file, synced from https://github.com/bvandrc/bvand
## Repo conventions

- **Package manager**: pnpm. `npm install` writes a competing `package-lock.json` that CI ignores.
- **Typed `Object` walks**: `typedKeys`, `typedEntries`, and `typedFromEntries`, exported from `src/utils/index.ts`, are ours. They are the builtins with the cast a call site would otherwise write by hand, so reach for one instead of asserting the result -- and leave `Object.keys`/`entries`/`fromEntries` alone where the widened type is what's wanted.
- **Convention files**: `conventions/` is synced from https://github.com/bvandrc/bvandrc-conventions by `.github/workflows/sync-conventions.yml` and overwritten on every sync. Edit a rule upstream, never in that directory.
15 changes: 0 additions & 15 deletions src/utils.ts

This file was deleted.

55 changes: 55 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Entries } from 'type-fest'

/**
* `Object.keys`, typed as the keys of what was passed.
*
* Narrower than the runtime guarantees: a value can carry keys beyond the ones
* its type names, which is the cast being made on purpose.
*
* Restricted to plain records: an array's `keyof` names its string methods, not
* the numeric-as-string indices `Object.keys` actually returns, so an array
* argument is refused rather than mistyped.
*/
export const typedKeys = <T extends Record<PropertyKey, unknown>>(o: T) =>
Object.keys(o) as (keyof T & string)[]

/**
* `Object.entries`, typed as the entries of what was passed.
*
* Carries the key-to-value correlation that `Object.entries` drops, so
* destructuring an entry of a union-valued object narrows. Refuses an array
* argument for the same reason as `typedKeys`.
*/
export const typedEntries = <T extends Record<PropertyKey, unknown>>(o: T) =>
Object.entries(o) as Entries<T>

/**
* `Object.fromEntries`, typed as the object those entries build.
*
* Only as precise as the entries it is handed, so a `.map` that wants each key
* paired with its own value type annotates the callback's return as a tuple.
*/
export const typedFromEntries = <
const EntryList extends readonly (readonly [PropertyKey, unknown])[],
>(
entries: EntryList
) =>
Object.fromEntries(entries) as {
[Entry in EntryList[number] as Entry[0]]: Entry[1]
}

export type AnyFunctionComponent = React.FunctionComponentElement<unknown>

/** The component's name, used to title the suite written for it. */
export const getComponentName = (Component: AnyFunctionComponent): string => {
const name =
Component.type.name ||
Component.type.displayName ||
(Component as unknown as { displayName?: string }).displayName
if (!name) {
throw new Error(
'Component has no name. May be a React.MemoExoticComponent-- if so, set displayName on the component.'
)
}
return name
}
Loading