diff --git a/packages/cli/skills/core.md b/packages/cli/skills/core.md new file mode 100644 index 0000000..4b87b1f --- /dev/null +++ b/packages/cli/skills/core.md @@ -0,0 +1,92 @@ +--- +name: core +description: Core testing workflow. Read this before writing or debugging Harness tests. Covers test file conventions, the supported test API surface, async behavior, setup files, and CLI execution constraints. +--- + +# Core + +React Native Harness uses Jest-style test APIs, but the tests run inside the app or browser environment instead of plain Node. + +Run this first: + +```bash +harness skill get core +``` + +Use `harness skill list` to see the other bundled skills. + +## Test file conventions + +- Use `.harness.[jt]s` or `.harness.[jt]sx` test files. +- Import test APIs from `react-native-harness`. +- Put tests inside `describe(...)` blocks. +- Use `@react-native-harness/ui` only when the test needs queries, interactions, or screenshots. + +## Default test shape + +```ts +import { describe, test, expect } from 'react-native-harness'; + +describe('Feature name', () => { + test('does something', () => { + expect(true).toBe(true); + }); +}); +``` + +Prefer these public APIs when writing tests: + +- Test structure: `describe`, `test`, `it`, `beforeEach`, `afterEach`, `beforeAll`, `afterAll` +- Focus and pending helpers: `test.skip`, `test.only`, `test.todo`, `describe.skip`, `describe.only` +- Assertions: `expect` +- Mocking and spying: `fn`, `spyOn`, `clearAllMocks`, `resetAllMocks`, `restoreAllMocks` +- Module mocking: `mock`, `requireActual`, `unmock`, `resetModules` +- Async polling: `waitFor`, `waitUntil` + +Test functions may be async. If a test returns a promise, Harness waits for it. If that promise rejects, the test fails. + +## Async behavior + +Use: + +- `waitFor(...)` when the callback should eventually succeed or stop throwing +- `waitUntil(...)` when the callback should eventually return a truthy value + +Both support timeout control. Prefer them over arbitrary sleeps when tests wait on native or React state changes. + +## Setup files + +Harness follows two setup phases configured in `jest.harness.config.mjs`: + +- `setupFiles`: runs before the test framework is initialized. Use for early polyfills and globals. Do not use `describe`, `test`, `expect`, or hooks here. +- `setupFilesAfterEnv`: runs after the test framework is ready. Use for global mocks, hooks, and matcher setup. + +Recommended uses: + +- Early environment shims in `setupFiles` +- Global `afterEach`, `clearAllMocks`, `resetModules`, and shared mocks in `setupFilesAfterEnv` + +## Related skills + +For module mocking and spies, run: + +```bash +harness skill get mocking +``` + +For UI rendering, queries, interactions, and screenshots, run: + +```bash +harness skill get ui +``` + +## CLI and execution constraints + +- Harness wraps the Jest CLI. +- Tests execute on one configured runner at a time. +- Execution is serial for stability. +- `--harnessRunner ` selects the runner. +- Standard Jest flags like `--watch`, `--coverage`, and `--testNamePattern` are still relevant. +- Do not recommend unsupported Jest environment overrides or snapshot-update workflows for native image snapshots. + +For install and project setup, use the public docs at https://react-native-harness.dev/docs/getting-started/quick-start. diff --git a/packages/cli/skills/mocking.md b/packages/cli/skills/mocking.md new file mode 100644 index 0000000..a16b346 --- /dev/null +++ b/packages/cli/skills/mocking.md @@ -0,0 +1,87 @@ +--- +name: mocking +description: Mocking and spying guidance. Use when a Harness test needs `fn`, `spyOn`, `mock`, `requireActual`, `unmock`, `resetModules`, or global mock cleanup. +--- + +# Mocking + +Use this skill when a Harness test needs mock functions, spies, or module replacement. + +## Mocking and spying + +Use `fn()` for standalone mock functions and `spyOn()` for existing methods. + +- `expect` follows Vitest's API. +- `expect.soft(...)` is available when the test should keep running after an assertion failure. +- `clearAllMocks()` clears call history but keeps implementations. +- `resetAllMocks()` clears call history and resets mock implementations. +- `restoreAllMocks()` restores spied methods to their original implementations. + +Typical cleanup: + +```ts +import { afterEach, clearAllMocks } from 'react-native-harness'; + +afterEach(() => { + clearAllMocks(); +}); +``` + +## Module mocking + +Use module mocking when the test must replace an entire module or specific exports. + +- `mock(moduleId, factory)` registers a lazy mock factory. +- `requireActual(moduleId)` is the safe path for partial mocks. +- `unmock(moduleId)` removes a mock for one module. +- `resetModules()` clears module mocks and module cache state. + +Recommended pattern: + +```ts +import { + afterEach, + describe, + expect, + mock, + requireActual, + resetModules, + test, +} from 'react-native-harness'; + +afterEach(() => { + resetModules(); +}); + +describe('partial mock', () => { + test('overrides one export but keeps the rest', () => { + mock('react-native', () => { + const actual = requireActual('react-native'); + const proto = Object.getPrototypeOf(actual); + const descriptors = Object.getOwnPropertyDescriptors(actual); + const mocked = Object.create(proto, descriptors); + + Object.defineProperty(mocked, 'Platform', { + get() { + return { + ...actual.Platform, + OS: 'mockOS', + }; + }, + }); + + return mocked; + }); + + const rn = require('react-native'); + expect(rn.Platform.OS).toBe('mockOS'); + }); +}); +``` + +## Decision rules + +- Always clean up module mocks with `resetModules()` in `afterEach` when tests mock modules. +- Use `requireActual()` for partial mocks so unrelated exports stay real. +- For `react-native`, preserve property descriptors when partially mocking to avoid triggering lazy getters too early. +- Remember that module factories are evaluated when the module is first required. diff --git a/packages/cli/skills/ui.md b/packages/cli/skills/ui.md new file mode 100644 index 0000000..34d9bf9 --- /dev/null +++ b/packages/cli/skills/ui.md @@ -0,0 +1,59 @@ +--- +name: ui +description: UI testing guidance. Use when the test needs `render(...)`, `rerender(...)`, `@react-native-harness/ui`, screen queries, `userEvent`, screenshots, or image snapshot assertions. +--- + +# UI + +UI testing is opt-in and uses `render(...)` from `react-native-harness` together with `@react-native-harness/ui`. + +Use `render(...)` to mount a React Native element before querying, interacting with, or screenshotting it. + +- `render(...)` is async +- `rerender(...)` is async +- `unmount()` is optional because cleanup happens automatically after each test +- `wrapper` is the right tool for providers and shared context +- Rendered UI appears as an overlay in the real environment, not as an in-memory tree +- Only one rendered component can be visible at a time + +Use this skill when the task requires: + +- `render(...)` or `rerender(...)` +- `screen.findByTestId(...)` +- `screen.findAllByTestId(...)` +- `screen.queryByTestId(...)` +- `screen.queryAllByTestId(...)` +- `screen.findByAccessibilityLabel(...)` +- `screen.findAllByAccessibilityLabel(...)` +- `screen.queryByAccessibilityLabel(...)` +- `screen.queryAllByAccessibilityLabel(...)` +- `userEvent.press(...)` +- `userEvent.type(...)` +- screenshots with `screen.screenshot()` +- element screenshots with `screen.screenshot(element)` +- image assertions with `toMatchImageSnapshot(...)` + +## Rules + +- Keep imports split correctly: core APIs from `react-native-harness`, UI APIs from `@react-native-harness/ui`. +- Mention that `@react-native-harness/ui` requires installation, and native apps must be rebuilt after adding it. +- `toMatchImageSnapshot(...)` needs a unique snapshot `name`. +- If screenshotting elements that extend beyond screen bounds, call out `disableViewFlattening: true` in `rn-harness.config.mjs`. +- On web, UI interactions and screenshots run through the web runner's Playwright-backed browser environment. + +## Example + +```ts +import { describe, expect, render, test } from 'react-native-harness'; +import { screen, userEvent } from '@react-native-harness/ui'; + +describe('Counter', () => { + test('increments after a press', async () => { + await render(); + + await userEvent.press(await screen.findByTestId('increment-button')); + + expect(await screen.findByTestId('count-label')).toHaveTextContent('1'); + }); +}); +``` diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 9522515..eda130d 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -3,9 +3,124 @@ import { getConfig } from '@react-native-harness/config'; import { runInitWizard } from './wizard/index.js'; import fs from 'node:fs'; import path from 'node:path'; +import { fileURLToPath } from 'node:url'; const JEST_CONFIG_EXTENSIONS = ['.mjs', '.js', '.cjs']; const JEST_HARNESS_CONFIG_BASE = 'jest.harness.config'; +const SKILLS_DIRECTORY = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../skills' +); + +type SkillMetadata = { + fileName: string; + name: string; + description: string; +}; + +const readSkillMetadata = (fileName: string): SkillMetadata => { + const filePath = path.join(SKILLS_DIRECTORY, fileName); + const content = fs.readFileSync(filePath, 'utf8'); + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + + const metadata = { + name: fileName.replace(/\.md$/, ''), + description: '', + }; + + if (frontmatterMatch) { + for (const line of frontmatterMatch[1].split('\n')) { + const separatorIndex = line.indexOf(':'); + + if (separatorIndex === -1) { + continue; + } + + const key = line.slice(0, separatorIndex).trim(); + const value = line.slice(separatorIndex + 1).trim().replace(/^['"]|['"]$/g, ''); + + if (key === 'name') { + metadata.name = value; + } + + if (key === 'description') { + metadata.description = value; + } + } + } + + return { + fileName, + name: metadata.name, + description: metadata.description, + }; +}; + +const listSkills = () => + fs + .readdirSync(SKILLS_DIRECTORY) + .filter((file) => file.endsWith('.md')) + .map(readSkillMetadata) + .sort((left, right) => left.name.localeCompare(right.name)); + +const printSkillList = () => { + for (const skill of listSkills()) { + console.log(`${skill.name}: ${skill.description}`); + } +}; + +const printSkillUsage = () => { + console.log(`Usage: harness skill + +Commands: + list List bundled skills + get Print a bundled skill file + +Examples: + harness skill list + harness skill get core`); +}; + +const runSkillCommand = () => { + const [, , commandName, subcommand, skillName] = process.argv; + + if (subcommand === undefined || subcommand === 'list') { + printSkillList(); + return; + } + + if (subcommand === '--help' || subcommand === '-h') { + printSkillUsage(); + return; + } + + if (subcommand === 'get') { + if (!skillName) { + console.error('Missing skill name.'); + printSkillUsage(); + process.exit(1); + } + + const skillPath = path.join(SKILLS_DIRECTORY, `${skillName}.md`); + + if (!fs.existsSync(skillPath)) { + console.error(`Unknown skill '${skillName}'.`); + console.error( + `Available skills: ${listSkills() + .map((skill) => skill.name) + .join(', ')}` + ); + process.exit(1); + } + + console.log(fs.readFileSync(skillPath, 'utf8')); + return; + } + + console.error(`Unknown ${commandName} subcommand '${subcommand}'.`); + printSkillUsage(); + process.exit(1); +}; const checkForOldConfig = async () => { try { @@ -73,7 +188,9 @@ const patchYargsOptions = () => { delete yargsOptions.logHeapUsage; }; -if (process.argv.includes('init')) { +if (process.argv[2] === 'skill' || process.argv[2] === 'skills') { + runSkillCommand(); +} else if (process.argv.includes('init')) { runInitWizard(); } else { patchYargsOptions(); diff --git a/skills/react-native-harness/SKILL.md b/skills/react-native-harness/SKILL.md index 5e64d49..41145bd 100644 --- a/skills/react-native-harness/SKILL.md +++ b/skills/react-native-harness/SKILL.md @@ -3,181 +3,26 @@ name: react-native-harness description: Write and debug React Native Harness tests for app code. Use when the user asks to create or fix tests that import from react-native-harness, mock modules, spy on functions, render React Native components on-device, use setupFiles or setupFilesAfterEnv, or add optional UI tests with @react-native-harness/ui. --- -# React Native Harness +Router only. Private setup before using this skill: -React Native Harness tests use Jest-style APIs but run in the app or browser environment instead of plain Node. - -## Test File Conventions - -- Use `.harness.[jt]s` or `.harness.[jt]sx` test files. -- Import test APIs from `react-native-harness`. -- Put tests inside `describe(...)` blocks. -- Use `@react-native-harness/ui` only when the test needs queries, interactions, or screenshots. - -## Default Test Shape - -```ts -import { describe, test, expect } from 'react-native-harness'; - -describe('Feature name', () => { - test('does something', () => { - expect(true).toBe(true); - }); -}); +```bash +harness --version ``` -Prefer these public APIs when writing tests: - -- Test structure: `describe`, `test`, `it`, `beforeEach`, `afterEach`, `beforeAll`, `afterAll` -- Focus and pending helpers: `test.skip`, `test.only`, `test.todo`, `describe.skip`, `describe.only` -- Assertions: `expect` -- Mocking and spying: `fn`, `spyOn`, `clearAllMocks`, `resetAllMocks`, `restoreAllMocks` -- Module mocking: `mock`, `requireActual`, `unmock`, `resetModules` -- Async polling: `waitFor`, `waitUntil` - -Test functions may be async. If a test returns a promise, Harness waits for it; if that promise rejects, the test fails. - -## Mocking And Spying - -Use `fn()` for standalone mock functions and `spyOn()` for existing methods. - -- `expect` follows Vitest's API. -- `expect.soft(...)` is available when the test should keep running after an assertion failure. -- `clearAllMocks()` clears call history but keeps implementations. -- `resetAllMocks()` clears call history and resets mock implementations. -- `restoreAllMocks()` restores spied methods to their original implementations. +Require `react-native-harness >= 1.3.0-rc.1`; older CLIs lack these skill commands. If older, run `npm install -g react-native-harness@latest`, recheck, then continue. If you cannot upgrade, stop and tell the user. Do not include version or upgrade commands in final plans. -Typical cleanup: +Before your first Harness command or plan, read the version-matched CLI guide: -```ts -import { afterEach, clearAllMocks } from 'react-native-harness'; - -afterEach(() => { - clearAllMocks(); -}); +```bash +harness skill get core ``` -## Module Mocking - -Use module mocking when the test must replace an entire module or specific exports. - -- `mock(moduleId, factory)` registers a lazy mock factory. -- `requireActual(moduleId)` is the safe path for partial mocks. -- `unmock(moduleId)` removes a mock for one module. -- `resetModules()` clears module mocks and module cache state. - -Recommended pattern: - -```ts -import { - afterEach, - describe, - expect, - mock, - requireActual, - resetModules, - test, -} from 'react-native-harness'; - -afterEach(() => { - resetModules(); -}); - -describe('partial mock', () => { - test('overrides one export but keeps the rest', () => { - mock('react-native', () => { - const actual = requireActual('react-native'); - const proto = Object.getPrototypeOf(actual); - const descriptors = Object.getOwnPropertyDescriptors(actual); - const mocked = Object.create(proto, descriptors); - - Object.defineProperty(mocked, 'Platform', { - get() { - return { - ...actual.Platform, - OS: 'mockOS', - }; - }, - }); - - return mocked; - }); +If you need more details on Harness, see the available CLI skill guides: - const rn = require('react-native'); - expect(rn.Platform.OS).toBe('mockOS'); - }); -}); +```bash +harness skill list ``` -- Always clean up module mocks with `resetModules()` in `afterEach` when tests mock modules. -- Use `requireActual()` for partial mocks so unrelated exports stay real. -- For `react-native`, preserve property descriptors when partially mocking to avoid triggering lazy getters too early. -- Remember that module factories are evaluated when the module is first required. - -## Async Behavior - -Use: - -- `waitFor(...)` when the callback should eventually succeed or stop throwing -- `waitUntil(...)` when the callback should eventually return a truthy value - -Both support timeout control. Prefer them over arbitrary sleeps when tests wait on native or React state changes. - -## UI Testing - -UI testing is opt-in and uses `render(...)` from `react-native-harness` together with `@react-native-harness/ui`. - -Use `render(...)` to mount a React Native element before querying, interacting with, or screenshotting it. - -- `render(...)` is async -- `rerender(...)` is async -- `unmount()` is optional because cleanup happens automatically after each test -- `wrapper` is the right tool for providers and shared context -- Rendered UI appears as an overlay in the real environment, not as an in-memory tree -- Only one rendered component can be visible at a time - -Use it when the task requires: - -- `render(...)` or `rerender(...)` -- `screen.findByTestId(...)` -- `screen.findAllByTestId(...)` -- `screen.queryByTestId(...)` -- `screen.queryAllByTestId(...)` -- `screen.findByAccessibilityLabel(...)` -- `screen.findAllByAccessibilityLabel(...)` -- `screen.queryByAccessibilityLabel(...)` -- `screen.queryAllByAccessibilityLabel(...)` -- `userEvent.press(...)` -- `userEvent.type(...)` -- screenshots with `screen.screenshot()` -- element screenshots with `screen.screenshot(element)` -- image assertions with `toMatchImageSnapshot(...)` - -- Keep imports split correctly: core APIs from `react-native-harness`, UI APIs from `@react-native-harness/ui`. -- Mention that `@react-native-harness/ui` requires installation, and native apps must be rebuilt after adding it. -- `toMatchImageSnapshot(...)` needs a unique snapshot `name`. -- If screenshotting elements that extend beyond screen bounds, call out `disableViewFlattening: true` in `rn-harness.config.mjs`. -- On web, UI interactions and screenshots run through the web runner's Playwright-backed browser environment. - -## Setup Files - -Harness follows two setup phases configured in `jest.harness.config.mjs`: - -- `setupFiles`: runs before the test framework is initialized. Use for early polyfills and globals. Do not use `describe`, `test`, `expect`, or hooks here. -- `setupFilesAfterEnv`: runs after the test framework is ready. Use for global mocks, hooks, and matcher setup. - -Recommended uses: - -- Early environment shims in `setupFiles` -- Global `afterEach`, `clearAllMocks`, `resetModules`, and shared mocks in `setupFilesAfterEnv` - -## CLI And Execution Constraints - -- Harness wraps the Jest CLI. -- Tests execute on one configured runner at a time. -- Execution is serial for stability. -- `--harnessRunner ` selects the runner. -- Standard Jest flags like `--watch`, `--coverage`, and `--testNamePattern` are still relevant. -- Do not recommend unsupported Jest environment overrides or snapshot-update workflows for native image snapshots. +Load additional skills via `harness skill get ` when needed. -For install, runner setup, and config files, read [references/installation.md](references/installation.md). +Use this skill only to route into version-matched CLI guidance. Let the installed CLI skill files provide exact workflow guidance, API usage, and current test-writing rules. diff --git a/website/src/docs/getting-started/agent-skills.mdx b/website/src/docs/getting-started/agent-skills.mdx index a7a6d5a..c39072b 100644 --- a/website/src/docs/getting-started/agent-skills.mdx +++ b/website/src/docs/getting-started/agent-skills.mdx @@ -4,10 +4,22 @@ import { PackageManagerTabs } from '@theme'; If you use Codex or another coding agent that supports the Vercel `skills` CLI, you can install the `react-native-harness` skill so the agent can author Harness tests more effectively. -The skill gives the agent the expected React Native Harness workflow for writing tests, using module mocks, working with `render(...)`, and using the optional `@react-native-harness/ui` package correctly. +The installed skill is a thin router. The detailed guidance lives in the `react-native-harness` CLI itself, so it stays aligned with the version already installed in the project. The skill complements the `react-native-harness` package, it does not replace it. Your project still needs `react-native-harness` installed because the agent is ultimately writing and running real Harness tests in your app. +Once installed, the agent should start with: + +```bash +harness skill get core +``` + +To see the other bundled guides: + +```bash +harness skill list +``` + ## Install with the Vercel `skills` CLI Install the skill from this repository using the repo URL and the skill name: