diff --git a/fern/docs.yml b/fern/docs.yml index 5bcfc43..7764c0a 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -114,6 +114,8 @@ navigation: contents: - page: Installation path: pages/docs/get-started/installation.mdx + - page: CLI reference + path: pages/docs/get-started/cli-reference.mdx - page: Concepts path: pages/docs/get-started/concepts.md - page: Create your first test @@ -129,8 +131,8 @@ navigation: - section: Configuration contents: - - page: Contexts - path: pages/docs/config/contexts.md + - page: Configuration + path: pages/docs/config/configuration.mdx - section: Tests contents: @@ -228,6 +230,24 @@ navigation: layout: - page: Glossary path: pages/reference/glossary.mdx + - section: Programmatic API + contents: + - page: Overview + path: pages/reference/api/overview.mdx + - page: runTests() + path: pages/reference/api/run-tests.mdx + - page: detectTests() + path: pages/reference/api/detect-tests.mdx + - page: detectAndResolveTests() + path: pages/reference/api/detect-and-resolve-tests.mdx + - page: resolveTests() + path: pages/reference/api/resolve-tests.mdx + - page: getRunner() + path: pages/reference/api/get-runner.mdx + - page: readFile() + path: pages/reference/api/read-file.mdx + - page: resolvePaths() + path: pages/reference/api/resolve-paths.mdx - folder: pages/reference/schemas title: Schemas diff --git a/fern/pages/contribute/templates/troubleshooting.mdx b/fern/pages/contribute/templates/troubleshooting.mdx index 679c7bb..c55b803 100644 --- a/fern/pages/contribute/templates/troubleshooting.mdx +++ b/fern/pages/contribute/templates/troubleshooting.mdx @@ -359,7 +359,7 @@ If you've tried these solutions and still have issues: ## See also -- [Context configuration](/docs/configuration/contexts) +- [Context configuration](/docs/configuration#contexts) {/*- [Supported browsers](/reference/browsers)*/} - [Browser options reference](/reference/schemas/browser) ```` diff --git a/fern/pages/contribute/templates/tutorial.mdx b/fern/pages/contribute/templates/tutorial.mdx index 13d8e49..7b4996a 100644 --- a/fern/pages/contribute/templates/tutorial.mdx +++ b/fern/pages/contribute/templates/tutorial.mdx @@ -423,7 +423,7 @@ Doc Detective needs a browser to run tests. Make sure: Now that you understand the basics: - [Add more actions to your tests](/docs/actions) -- [Learn about test contexts](/docs/configuration/contexts) +- [Learn about test contexts](/docs/configuration#contexts) - [Write tests for your own documentation](/contribute/testing) ```` diff --git a/fern/pages/docs/config/configuration.mdx b/fern/pages/docs/config/configuration.mdx new file mode 100644 index 0000000..513f7c6 --- /dev/null +++ b/fern/pages/docs/config/configuration.mdx @@ -0,0 +1,596 @@ +--- +title: Configuration +description: Configure Doc Detective's behavior, including input/output, contexts, file types, integrations, and more. +--- + +Doc Detective uses a configuration file to control its behavior. You can set options for test input and output, execution contexts, file type handling, integrations, and more. + +## Config file basics + +Doc Detective looks for a configuration file in the current directory in the following order: + +1. `.doc-detective.json` +2. `.doc-detective.yaml` +3. `.doc-detective.yml` + +You can also specify a configuration file with the `--config` CLI flag or the `DOC_DETECTIVE_CONFIG` environment variable. + +The configuration file can be JSON or YAML. A JSON Schema is available at [the `config` schema](/reference/schemas/config) for validation and autocompletion in your editor. + +Here's a minimal configuration file: + +```json +{ + "input": ".", + "output": "output" +} +``` + +## Core options + +These options control the basic input and output behavior: + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `input` | string or array | `"."` | Files, directories, or URLs to test. | +| `output` | string | `"."` | Directory for test results. | +| `recursive` | boolean | `true` | Whether to search directories recursively for test files. | +| `relativePathBase` | string | `"file"` | Base for resolving relative paths: `"file"` (relative to the config file) or `"cwd"` (relative to the current working directory). | + +```json +{ + "input": ["./docs", "./guides"], + "output": "./test-results", + "recursive": true, + "relativePathBase": "file" +} +``` + +## Test execution + +These options control how Doc Detective runs tests: + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `detectSteps` | boolean | `true` | Whether to auto-detect test steps from documentation content. | +| `allowUnsafeSteps` | boolean | `false` | Whether to allow `runShell` and `runCode` actions. | +| `concurrentRunners` | number | `1` | Number of browsers to run concurrently for parallel testing. | + +```json +{ + "input": ".", + "detectSteps": true, + "allowUnsafeSteps": false, + "concurrentRunners": 2 +} +``` + +## Contexts + +Contexts define *where* tests run. A context specifies a combination of a target platform (operating system) and, optionally, a target browser with specific configurations. + +By default, if contexts are needed but not specified, Doc Detective attempts to find a supported browser (like Chrome or Firefox) on the current platform (Windows, macOS, or Linux) and run tests there. + +For comprehensive options, see the [`context` schema](/reference/schemas/context). + +### Defining contexts + +You define contexts using a `runOn` array of context objects. Each context object specifies target `platforms` (as a string or array) and target `browsers` (as a string, array, or object). + +```json +{ + "runOn": [ + { + "platforms": ["windows", "mac", "linux"], + "browsers": "chrome" + }, + { + "platforms": ["windows", "mac", "linux"], + "browsers": "firefox" + }, + { + "platforms": "mac", + "browsers": "webkit" + } + ] +} +``` + +### Context precedence + +You can specify contexts at three levels, in order of precedence: + +1. **Test**: Contexts defined within a specific [`test`](/reference/schemas/test) override all other contexts and apply only to that test. +2. **Spec**: Contexts defined in a [`specification`](/reference/schemas/specification) override config-level contexts and apply to all tests within that spec unless overridden. +3. **Config**: Contexts defined in the main [`config`](/reference/schemas/config) apply to all tests unless overridden. + +### Browsers + +Doc Detective can perform browser-based tests on the following engines: + +- **Chrome** (`chrome`): Uses Chromium. Available on Windows, macOS, and Linux. The only browser that currently supports video recording via the [`record`](/docs/actions/record) action. +- **Firefox** (`firefox`): Uses Firefox. Available on Windows, macOS, and Linux. +- **WebKit** (`webkit` or `safari`): Uses WebKit. Primarily associated with Safari on macOS. + +#### Chrome + +Here's a basic Chrome context: + +```json +{ + "platforms": ["windows", "mac", "linux"], + "browsers": "chrome" +} +``` + +With dimensions and visibility: + +```json +{ + "platforms": ["windows", "mac", "linux"], + "browsers": { + "name": "chrome", + "headless": false, + "window": { + "width": 1280, + "height": 800 + }, + "viewport": { + "width": 1200, + "height": 720 + } + } +} +``` + + +Set `headless` to `false` (headed mode) to use the `record` action for video capture. + + +#### Firefox + +Here's a basic Firefox context: + +```json +{ + "platforms": ["windows", "mac", "linux"], + "browsers": "firefox" +} +``` + +With dimensions and visibility: + +```json +{ + "platforms": ["windows", "mac", "linux"], + "browsers": { + "name": "firefox", + "headless": true, + "window": { + "width": 1024, + "height": 768 + } + } +} +``` + +#### WebKit / Safari + +You can use either `webkit` or `safari` as the browser name. + +Before running tests with WebKit/Safari on macOS, you might need to enable the driver: + +1. Run `safaridriver --enable` in your terminal. +2. Ensure **Develop > Allow Remote Automation** is checked in Safari's menu bar (you might need to enable the Develop menu first in Safari's Advanced preferences). + + +This setup is often handled automatically in CI environments like GitHub Actions. + + +```json +{ + "platforms": "mac", + "browsers": "webkit" +} +``` + +WebKit/Safari does **not** support headless mode: + +```json +{ + "platforms": "mac", + "browsers": { + "name": "webkit", + "headless": false, + "viewport": { + "width": 1440, + "height": 900 + } + } +} +``` + +### Platforms + +Doc Detective supports the following platforms: + +- **Windows** (`windows`) +- **macOS** (`mac`) +- **Linux** (`linux`) — tested primarily on Ubuntu + +When you specify a platform in a context, Doc Detective runs the associated tests only on a matching operating system. If `platforms` is omitted, it defaults to the current platform. + +### Responsive design testing + +You can create contexts with different viewport sizes to test responsive layouts: + +```json +{ + "runOn": [ + { + "platforms": ["windows", "mac", "linux"], + "browsers": { + "name": "chrome", + "viewport": { "width": 1920, "height": 1080 } + } + }, + { + "platforms": ["windows", "mac", "linux"], + "browsers": { + "name": "chrome", + "viewport": { "width": 768, "height": 1024 } + } + }, + { + "platforms": ["windows", "mac", "linux"], + "browsers": { + "name": "chrome", + "viewport": { "width": 375, "height": 812 } + } + } + ] +} +``` + +### Contexts in a config + +Contexts defined at the config level apply to all tests unless overridden by spec- or test-level contexts. + +```json +{ + "input": ".", + "output": "output", + "runOn": [ + { + "platforms": ["windows", "mac", "linux"], + "browsers": "chrome" + }, + { + "platforms": ["windows", "mac", "linux"], + "browsers": "firefox" + }, + { + "platforms": "mac", + "browsers": { + "name": "webkit", + "window": { "width": 1280, "height": 800 } + } + } + ] +} +``` + +### Contexts in a specification + +Contexts in a spec override config-level contexts for all tests in that spec. + +```json +{ + "description": "Login tests", + "runOn": [ + { + "platforms": ["windows", "mac"], + "browsers": "chrome" + } + ], + "tests": [] +} +``` + +### Contexts in a test + +Contexts in a test override both config- and spec-level contexts for that specific test. + +```json +{ + "description": "Main application specification", + "tests": [ + { + "description": "Test login form on Windows/Chrome only", + "runOn": [ + { + "platforms": "windows", + "browsers": "chrome" + } + ], + "steps": [] + }, + { + "description": "Test dashboard on all default contexts", + "steps": [] + } + ] +} +``` + +### CI/CD matrix testing + +Use contexts with CI/CD matrix strategies to test across multiple platforms and browsers: + +```yaml +# GitHub Actions matrix example +jobs: + test: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: doc-detective/github-action@v1 +``` + +## Setup and cleanup + +Use these options to run actions before and after your test suite: + +| Option | Type | Description | +|--------|------|-------------| +| `beforeAny` | string or array of string | Test specification file(s) to run before any tests start. | +| `afterAll` | string or array of string | Test specification file(s) to run after all tests complete. | + +```json +{ + "input": ".", + "beforeAny": ["./setup.spec.json"], + "afterAll": ["./cleanup.spec.json"] +} +``` + +## Logging and debugging + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `logLevel` | string | `"info"` | Log verbosity: `silent`, `error`, `warning`, `info`, or `debug`. | + +```json +{ + "input": ".", + "logLevel": "debug" +} +``` + +## Advanced options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `origin` | string | `""` | Default protocol and domain prepended to relative paths in `goTo` actions. | +| `crawl` | boolean | `false` | Whether to automatically crawl links in the input files. | + +```json +{ + "input": ".", + "origin": "https://docs.example.com", + "crawl": false +} +``` + +## File type configuration + +Doc Detective determines how to parse input files based on file type definitions. You can customize file type handling to support additional file extensions or inline test patterns. + +Each file type definition includes: + +| Property | Description | +|----------|-------------| +| `extensions` | File extensions associated with this type (such as `.md`, `.mdx`). | +| `testStartStatementOpen` | Opening pattern for inline test declarations. | +| `testStartStatementClose` | Closing pattern for inline test declarations. | +| `testEndStatement` | Pattern for test end declarations. | +| `stepStatementOpen` | Opening pattern for inline step declarations. | +| `stepStatementClose` | Closing pattern for inline step declarations. | +| `testIgnoreStatement` | Pattern for test ignore blocks. | +| `markupStatement` | Patterns for detecting markup elements (links, images, code blocks). | + +```json +{ + "fileTypes": [ + { + "extensions": [".rst"], + "testStartStatementOpen": ".. test:: ", + "testStartStatementClose": "", + "testEndStatement": ".. test-end::", + "stepStatementOpen": ".. step:: ", + "stepStatementClose": "" + } + ] +} +``` + +For more details, see [Custom input formats](/docs/input-formats/custom). + +## Integration configuration + +Doc Detective supports integrations with external tools and services: + +### OpenAPI + +Automatically resolve relative URLs in `httpRequest` actions using OpenAPI definitions: + +```json +{ + "integrations": { + "openApi": [ + { + "descriptionPath": "./openapi.yaml", + "server": "https://api.example.com" + } + ] + } +} +``` + +## Telemetry + +Doc Detective collects anonymous usage telemetry to improve the product. You can configure telemetry behavior: + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `telemetry.send` | boolean | `true` | Whether to send anonymous telemetry data. | +| `telemetry.userId` | string | Auto-generated | An anonymous user identifier for telemetry. | + +```json +{ + "telemetry": { + "send": false + } +} +``` + +## Environment variables in config + +You can reference environment variables in your configuration file using the `$VARIABLE_NAME` syntax: + +```json +{ + "input": ".", + "origin": "$SITE_URL", + "integrations": { + "openApi": [ + { + "server": "$API_SERVER" + } + ] + } +} +``` + +Doc Detective resolves environment variables at runtime, so you can keep sensitive values out of your configuration file. + +## Environment-specific configs + +Create separate configuration files for different environments. For example, a CI-specific config with headless browsers: + +`.doc-detective.ci.json`: + +```json +{ + "input": ".", + "output": "./test-results", + "logLevel": "error", + "runOn": [ + { + "platforms": ["linux"], + "browsers": { + "name": "chrome", + "headless": true + } + } + ] +} +``` + +Then use it in your CI pipeline: + +```bash +npx doc-detective --config .doc-detective.ci.json +``` + +## Real-world examples + +### Simple site testing + +```json +{ + "input": "./docs", + "output": "./test-results", + "origin": "https://docs.example.com" +} +``` + +### Multi-browser testing + +```json +{ + "input": ".", + "output": "./test-results", + "runOn": [ + { + "platforms": ["windows", "mac", "linux"], + "browsers": { + "name": "chrome", + "headless": true, + "viewport": { "width": 1280, "height": 720 } + } + }, + { + "platforms": ["windows", "mac", "linux"], + "browsers": { + "name": "firefox", + "headless": true + } + }, + { + "platforms": "mac", + "browsers": "webkit" + } + ] +} +``` + +### CI/CD configuration + +```json +{ + "input": ".", + "output": "./test-results", + "logLevel": "error", + "concurrentRunners": 2, + "runOn": [ + { + "platforms": ["linux"], + "browsers": { + "name": "chrome", + "headless": true + } + }, + { + "platforms": ["linux"], + "browsers": { + "name": "firefox", + "headless": true + } + } + ] +} +``` + +### Development configuration + +```json +{ + "input": ".", + "output": "./test-results", + "logLevel": "debug", + "runOn": [ + { + "platforms": ["windows", "mac", "linux"], + "browsers": { + "name": "chrome", + "headless": false, + "viewport": { "width": 1280, "height": 720 } + } + } + ] +} +``` diff --git a/fern/pages/docs/config/contexts.md b/fern/pages/docs/config/contexts.md deleted file mode 100644 index 55eba41..0000000 --- a/fern/pages/docs/config/contexts.md +++ /dev/null @@ -1,310 +0,0 @@ ---- -title: Contexts -description: Define the contexts (platform and browser combinations) where tests should run. ---- - -Doc Detective uses contexts to determine *where* tests should run. A context defines a combination of a target platform (operating system) and, optionally, a target browser with specific configurations. - -By default, if contexts are needed but not specified, Doc Detective attempts to find a supported browser (like Chrome or Firefox) on the current platform (Windows, macOS, or Linux) and run tests there. - -You define contexts using an array of context objects. Each context object specifies the target `platforms` (as a string or array) and the target `browsers` (as a string, array, or object). - -When Doc Detective runs tests, it evaluates the defined contexts against the current environment. If the current platform matches one specified in a context, and if a browser is specified and available, the test runs in that specific browser on that platform. You can specify multiple contexts, and Doc Detective will attempt to run the relevant tests in each matching context. - -For comprehensive options, see the [context](/reference/schemas/context) reference. - -## Specifying contexts - -You can specify contexts at three different levels, in order of precedence: - -- **Config**: Contexts defined in the main [`config`](/reference/schemas/config) apply to all tests unless overridden. -- **Spec**: Contexts defined in a [`specification`](/reference/schemas/specification) override config-level contexts and apply to all tests within that spec unless overridden. -- **Test**: Contexts defined within a specific [`test`](/reference/schemas/test) override config- and spec-level contexts and apply only to that test. - -Contexts are defined using a `runOn` array containing context objects. For example: - -```json -{ - ... - "runOn": [ - { - "platforms": ["windows", "mac", "linux"], - "browsers": "chrome" - }, - { - "platforms": ["windows", "mac", "linux"], - "browsers": "firefox" - }, - { - "platforms": "mac", - "browsers": "webkit" // or "safari" - } - ], - ... -} -``` - -## Browsers - -Doc Detective can perform browser-based tests on several browser engines. The following browser names are supported in the `browsers` property: - -- **Chrome** (`chrome`): Uses Chromium. -- **Firefox** (`firefox`): Uses Firefox. -- **WebKit** (`webkit`): Uses WebKit. The name `safari` can be used as an alias for `webkit`. - - -### Chrome (`chrome`) - -Available on Windows, macOS, and Linux. - -Chrome is the only browser that currently supports video recording via the [`record`](/docs/actions/record) action. - -Here's a basic Chrome context for all platforms: - -```json -{ - "platforms": ["windows", "mac", "linux"], - "browsers": "chrome" -} -``` - -Or using the object format: - -```json -{ - "platforms": ["windows", "mac", "linux"], - "browsers": { - "name": "chrome" - } -} -``` - -#### Chrome Dimensions and Visibility - -You can specify browser window dimensions, viewport dimensions, and visibility (`headless`). `headless` must be `false` (that is, run in headed mode) to use the `record` action. - -```json -{ - "platforms": ["windows", "mac", "linux"], - "browsers": { - "name": "chrome", - "headless": false, // Required for recording - "window": { - "width": 1280, - "height": 800 - }, - "viewport": { - "width": 1200, - "height": 720 - } - } -} -``` - -### Firefox (`firefox`) - -Available on Windows, macOS, and Linux. - -Here's a basic Firefox context: - -```json -{ - "platforms": ["windows", "mac", "linux"], - "browsers": "firefox" -} -``` - -#### Firefox Dimensions and Visibility - -You can specify dimensions and visibility (`headless`). - -```json -{ - "platforms": ["windows", "mac", "linux"], - "browsers": { - "name": "firefox", - "headless": true, - "window": { - "width": 1024, - "height": 768 - } - } -} -``` - -### WebKit (`webkit` or `safari`) - -WebKit testing is primarily associated with Safari on macOS. Doc Detective runs tests using the WebKit driver. - -You can use either `webkit` or `safari` as the browser name. - -Before running tests with WebKit/Safari on macOS, you might need to enable the driver: - -1. Run `safaridriver --enable` in your terminal. -2. Ensure **Develop > Allow Remote Automation** is checked in Safari's menu bar (you might need to enable the Develop menu first in Safari's Advanced preferences). - -*Note: This setup is often handled automatically in CI environments like GitHub Actions.* - -Here's a basic WebKit/Safari context for macOS: - -```json -{ - "platforms": "mac", - "browsers": "webkit" // or "safari" -} -``` - -#### WebKit/Safari Dimensions - -You can specify window or viewport dimensions. WebKit/Safari does **not** support headless mode. - -```json -{ - "platforms": "mac", - "browsers": { - "name": "webkit", // or "safari" - "headless": false, // Headless is not supported - "viewport": { - "width": 1440, - "height": 900 - } - } -} -``` - -## Platforms - -Doc Detective can run tests targeting the following platforms: - -- Windows (`windows`) -- macOS (`mac`) -- Linux (`linux`) (Tested primarily on Ubuntu) - -When you specify a platform (or multiple platforms) in a context, Doc Detective attempts to run the associated tests only when executed on a matching operating system. If `platforms` is omitted, it defaults to the current platform. - -For example, this context targets only macOS: - -```json -{ - "platforms": "mac", - "browsers": "chrome" -} -``` - -This context targets Windows or Linux: - -```json -{ - "platforms": ["windows", "linux"], - "browsers": "firefox" -} -``` - -## Examples - -### Basic Contexts - -- Run tests in Chrome on all supported platforms: - - ```json - { - "platforms": ["windows", "mac", "linux"], - "browsers": "chrome" - } - ``` - -- Run tests in Firefox on Windows and macOS: - - ```json - { - "platforms": ["windows", "mac"], - "browsers": "firefox" - } - ``` - -- Run tests in WebKit/Safari on macOS: - - ```json - { - "platforms": "mac", - "browsers": "webkit" // or "safari" - } - ``` - -### Contexts in a Config (`config.json`) - -Specify contexts in the top-level `runOn` array. These apply to all tests unless overridden. - -```json -{ - "input": ".", - "output": "output", - "runOn": [ - { - "platforms": ["windows", "mac", "linux"], - "browsers": "chrome" - }, - { - "platforms": ["windows", "mac", "linux"], - "browsers": "firefox" - }, - { - "platforms": "mac", - "browsers": { - "name": "webkit", - "window": { "width": 1280, "height": 800 } - } - } - ] -} -``` - -### Contexts in a Specification (`*.spec.json`) - -Specify contexts in the spec's `runOn` array. These override config-level contexts for tests within this spec. - -```json -{ - "description": "Specification for login tests", - "runOn": [ - { - "platforms": ["windows", "mac"], - "browsers": "chrome" - } - ], - "tests": [ - // ... tests in this spec will run on Chrome on Windows & Mac - ] -} -``` - -### Contexts in a Test - -Specify contexts in the test's `runOn` array. These override config- and spec-level contexts for this specific test. - -```json -{ - "description": "Main application specification", - "tests": [ - { - "description": "Test login form on Windows/Chrome only", - "runOn": [ - { - "platforms": "windows", - "browsers": "chrome" - } - ], - "steps": [ - // ... steps for this test - ] - }, - { - "description": "Test dashboard on all default contexts", - // No runOn here, inherits from spec or config - "steps": [ - // ... steps for this test - ] - } - ] -} -``` diff --git a/fern/pages/docs/get-started/ci.mdx b/fern/pages/docs/get-started/ci.mdx index 383bd7a..c65db5f 100644 --- a/fern/pages/docs/get-started/ci.mdx +++ b/fern/pages/docs/get-started/ci.mdx @@ -157,3 +157,262 @@ jobs: - uses: actions/checkout@v4 - uses: doc-detective/github-action@v1 ``` + +## Other CI/CD platforms + +You can run Doc Detective in any CI/CD platform that supports Node.js. The following examples show how to set up Doc Detective in popular CI/CD platforms. + +### GitLab CI/CD + +Create a `.gitlab-ci.yml` file in your repository root: + +```yaml +doc-detective: + image: node:22 + script: + - npm i -g doc-detective + - npx doc-detective --config .doc-detective.ci.json + artifacts: + paths: + - test-results/ + when: always +``` + +Multi-platform GitLab CI: + +```yaml +.doc-detective-base: + stage: test + script: + - npm i -g doc-detective + - npx doc-detective --config .doc-detective.ci.json + +doc-detective-linux: + extends: .doc-detective-base + image: node:22 + tags: + - linux + +doc-detective-windows: + extends: .doc-detective-base + tags: + - windows +``` + +### Jenkins + +Add a stage to your `Jenkinsfile`: + +```groovy +pipeline { + agent { docker { image 'node:22' } } + stages { + stage('Doc Detective') { + steps { + sh 'npm i -g doc-detective' + sh 'npx doc-detective --input ./docs' + } + post { + always { + archiveArtifacts artifacts: 'test-results/**', allowEmptyArchive: true + } + } + } + } +} +``` + +### CircleCI + +Create a `.circleci/config.yml` file: + +```yaml +version: 2.1 +jobs: + doc-detective: + docker: + - image: cimg/node:22.0 + steps: + - checkout + - run: + name: Install Doc Detective + command: npm i -g doc-detective + - run: + name: Run tests + command: npx doc-detective --input ./docs + - store_artifacts: + path: test-results + +workflows: + test: + jobs: + - doc-detective +``` + +### Azure Pipelines + +Create an `azure-pipelines.yml` file: + +```yaml +trigger: + - main + +pool: + vmImage: "ubuntu-latest" + +steps: + - task: NodeTool@0 + inputs: + versionSpec: "22.x" + - script: npm i -g doc-detective + displayName: "Install Doc Detective" + - script: npx doc-detective --input ./docs + displayName: "Run Doc Detective" + - publish: test-results + artifact: doc-detective-results + condition: always() +``` + +Multi-platform Azure Pipelines: + +```yaml +trigger: + - main + +strategy: + matrix: + linux: + vmImage: "ubuntu-latest" + mac: + vmImage: "macos-latest" + windows: + vmImage: "windows-latest" + +pool: + vmImage: $(vmImage) + +steps: + - task: NodeTool@0 + inputs: + versionSpec: "22.x" + - script: npm i -g doc-detective + displayName: "Install Doc Detective" + - script: npx doc-detective --config .doc-detective.ci.json + displayName: "Run Doc Detective" +``` + +### Travis CI + +Create a `.travis.yml` file: + +```yaml +language: node_js +node_js: + - "22" + +os: + - linux + - osx + - windows + +install: + - npm i -g doc-detective + +script: + - npx doc-detective --input ./docs +``` + +## Docker usage + +You can use the Doc Detective Docker image in any CI/CD platform: + +```bash +docker run -v .:/app docdetective/docdetective --input /app/docs +``` + +In CI environments, mount your workspace directory to `/app` in the container so Doc Detective can access your test files and write results. + +## CI-specific configuration + +Create a dedicated configuration file for CI environments with headless browsers and error-level logging: + +**.doc-detective.ci.json**: + +```json +{ + "input": ".", + "output": "./test-results", + "logLevel": "error", + "runOn": [ + { + "platforms": ["linux"], + "browsers": { + "name": "chrome", + "headless": true + } + } + ] +} +``` + +Then reference it in your CI pipeline: + +```bash +npx doc-detective --config .doc-detective.ci.json +``` + +## Secrets and environment variables + +Pass sensitive values through environment variables instead of hardcoding them in configuration files. Doc Detective supports environment variable references with the `$VARIABLE_NAME` syntax. + +For example, if your tests need an API key: + +```json +{ + "input": ".", + "origin": "$SITE_URL" +} +``` + +Set the environment variable in your CI platform's settings or pipeline definition. + +## Handle test failures + +### Fail the pipeline + +Check exit codes to fail the pipeline when tests fail: + +```bash +npx doc-detective --input ./docs +if [ $? -ne 0 ]; then + echo "Documentation tests failed!" + exit 1 +fi +``` + +### Upload artifacts + +Preserve test results as artifacts for debugging: + +```yaml +# GitHub Actions +- uses: actions/upload-artifact@v4 + if: always() + with: + name: doc-detective-results + path: test-results/ +``` + +### Notifications + +Send notifications on test failure using your CI platform's notification features. For example, with GitHub Actions and Slack: + +```yaml +- uses: slackapi/slack-github-action@v1 + if: failure() + with: + channel-id: "docs-team" + slack-message: "Doc Detective tests failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} +``` diff --git a/fern/pages/docs/get-started/cli-reference.mdx b/fern/pages/docs/get-started/cli-reference.mdx new file mode 100644 index 0000000..f9a294c --- /dev/null +++ b/fern/pages/docs/get-started/cli-reference.mdx @@ -0,0 +1,114 @@ +--- +title: CLI reference +sidebar-title: CLI reference +description: Command-line interface flags, environment variables, and usage examples for Doc Detective. +--- + +Doc Detective provides a command-line interface (CLI) for running documentation tests. You can run it with `npx` or through Docker. + +## Basic usage + + + + +```bash +npx doc-detective [options] +``` + + + + +```bash +docker run -v .:/app docdetective/docdetective [options] +``` + + + + +## Options + +| Flag | Alias | Description | Default | +|------|-------|-------------|---------| +| `--config` | `-c` | Path to a configuration file. Doc Detective looks for `.doc-detective.json`, `.doc-detective.yaml`, or `.doc-detective.yml` in the current directory if not specified. | Auto-detected | +| `--input` | `-i` | Files, directories, or URLs to test. Separate multiple values with commas. | Config's `input` value | +| `--output` | `-o` | Directory for test results. | Config's `output` value | +| `--logLevel` | `-l` | Log verbosity level: `silent`, `error`, `warning`, `info`, or `debug`. | `info` | +| `--allow-unsafe` | | Allow potentially unsafe actions like `runShell` and `runCode`. Required to run these actions. | `false` | +| `--help` | `-h` | Display help information. | | +| `--version` | | Display the current Doc Detective version. | | + +## Exit codes + +Doc Detective returns the following exit codes: + +| Code | Meaning | +|------|---------| +| `0` | All tests passed (or completed with warnings). | +| `1` | One or more tests failed or an error occurred. | + +You can use exit codes in scripts to take action based on test results: + +```bash +npx doc-detective --input ./docs +if [ $? -ne 0 ]; then + echo "Documentation tests failed!" + exit 1 +fi +``` + +## Environment variables + +Doc Detective recognizes the following environment variables: + +| Variable | Description | +|----------|-------------| +| `DOC_DETECTIVE_CONFIG` | Path to a configuration file. Equivalent to `--config`. | +| `DOC_DETECTIVE_API` | API key for Doc Detective integrations. | +| `DOC_DETECTIVE_META` | Metadata to include with test results. | + +## Configuration precedence + +When the same option is set in multiple places, Doc Detective applies settings in this order (highest priority first): + +1. **CLI arguments** — Flags passed directly on the command line. +2. **Environment variables** — Values set in the shell environment. +3. **Configuration file** — Values in `.doc-detective.json`, `.doc-detective.yaml`, or `.doc-detective.yml`. +4. **Defaults** — Built-in default values. + +## Examples + +Run tests with default settings: + +```bash +npx doc-detective +``` + +Specify a configuration file: + +```bash +npx doc-detective --config .doc-detective.ci.json +``` + +Test multiple inputs: + +```bash +npx doc-detective --input ./docs,./guides,./tutorials +``` + +Set log level to debug for troubleshooting: + +```bash +npx doc-detective --logLevel debug +``` + +Allow unsafe operations like shell commands: + +```bash +npx doc-detective --allow-unsafe --input ./tests +``` + +Output results to a specific directory: + +```bash +npx doc-detective --output ./test-results +``` diff --git a/fern/pages/docs/get-started/installation.mdx b/fern/pages/docs/get-started/installation.mdx index 8e42a08..187751b 100644 --- a/fern/pages/docs/get-started/installation.mdx +++ b/fern/pages/docs/get-started/installation.mdx @@ -79,3 +79,61 @@ You should see output in the terminal indicating the progress and results of the ## Next steps Next up, take a look at some of the [concepts](/docs/get-started/concepts) native to Doc Detective, check out some of the guided [tutorials](/tutorials), or explore how to write [tests](/docs/tests) and [actions](/docs/actions) to automate your documentation testing. + +## Typical project structure + +A common project layout when using Doc Detective: + +``` +my-docs-project/ +├── .doc-detective.json # Configuration file +├── docs/ +│ ├── getting-started.md +│ ├── getting-started.spec.json +│ ├── api-reference.md +│ └── api-reference.spec.json +├── test-results/ # Generated test results +└── screenshots/ # Generated screenshots +``` + +## Troubleshooting + +### Command not found + +If you get a "command not found" error after installing, make sure your npm global `bin` directory is in your system's `PATH`: + +```bash +npm config get prefix +``` + +Add the displayed path's `bin` subdirectory to your `PATH`. + +### Permission errors + +On macOS and Linux, if you get permission errors during installation, avoid using `sudo`. Instead, configure npm to use a different directory: + +```bash +mkdir ~/.npm-global +npm config set prefix '~/.npm-global' +``` + +Then add `~/.npm-global/bin` to your `PATH`. + +Alternatively, use [nvm](https://github.com/nvm-sh/nvm) to manage Node.js installations without requiring elevated permissions. + +### Browser launch issues + +If Doc Detective can't launch a browser: + +- Make sure Chrome or Firefox is installed on your system. +- On Linux, install required dependencies: `sudo apt-get install -y libgbm-dev libasound2`. +- In CI environments, use headless mode in your [configuration](/docs/configuration). + +### Module not found + +If you see "module not found" errors, try reinstalling: + +```bash +npm uninstall -g doc-detective +npm i -g doc-detective +``` diff --git a/fern/pages/docs/tests/inline.mdx b/fern/pages/docs/tests/inline.mdx index 21d84ae..3977e08 100644 --- a/fern/pages/docs/tests/inline.mdx +++ b/fern/pages/docs/tests/inline.mdx @@ -175,3 +175,112 @@ Click the *Start* button: // (test end) ``` + +## Combine explicit and detected tests + +You can mix inline tests with [detected tests](./detected-tests) in the same file. Declare explicit tests for specific workflows, and let Doc Detective auto-detect steps for the rest of the content. + +For example, you might explicitly test a multi-step login flow while letting detected tests handle link checking for the rest of the page: + +```markdown +{/* test {"testId": "login-flow", "detectSteps": false} */} + +1. Go to the [login page](https://app.example.com/login). + + {/* step {"goTo": "https://app.example.com/login"} */} + +2. Enter your credentials and click **Sign in**. + + {/* step {"find": "input[name='email']"} */} + {/* step {"type": ["user@example.com"]} */} + {/* step {"click": "button[type='submit']"} */} + +{/* test end */} + +The rest of this page has links that Doc Detective can auto-detect and check. +``` + +## Record media in inline tests + +You can capture screenshots and record videos using inline steps: + +```markdown +{/* test {"testId": "capture-demo"} */} + +Navigate to the homepage: + +{/* step {"goTo": "https://example.com"} */} + +Here's what the homepage looks like: + +![Homepage](homepage.png) + +{/* step {"screenshot": "homepage.png"} */} + +{/* test end */} +``` + +For video recording, use `record` and `stopRecord`: + +```markdown +{/* test {"testId": "video-demo", "detectSteps": false} */} + +{/* step {"record": "demo-video.webm"} */} + +1. Go to the site. + + {/* step {"goTo": "https://example.com"} */} + +2. Click **Get Started**. + + {/* step {"click": "Get Started"} */} + {/* step {"wait": 2000} */} + +{/* step {"stopRecord": ""} */} + +{/* test end */} +``` + + +Video recording requires Chrome in non-headless mode. See [Contexts](/docs/configuration#contexts) for browser configuration. + + +## Best practices + +- **Keep steps simple**: Each step comment should contain a single action. Avoid combining multiple actions in one step. +- **Use detection wisely**: Set `detectSteps` to `false` on tests where you want full control over what's tested. +- **Add wait steps**: Insert `wait` steps after actions that trigger page loads or animations. +- **Use test boundaries**: Always close tests with a test end comment to prevent unintended steps from being included. +- **Minimize inline tests**: For complex test scenarios, consider using [standalone tests](./standalone-tests) instead. + +## Troubleshooting + +### Tests not detected + +If your inline tests aren't being picked up: + +- Verify the comment syntax matches your file type's patterns. +- Check that your [configuration](/docs/configuration) includes the file's extension in its file type definitions. +- Make sure each comment is on a single line. Doc Detective doesn't support multi-line test or step comments. + +### Wrong execution order + +Steps execute in the order they appear in the file. If steps seem out of order: + +- Review the file for any unintentionally included comments. +- Make sure test boundaries (`test end`) are placed correctly. + +### Screenshots not capturing + +If screenshots aren't saved: + +- Check the file path in the `screenshot` step. Relative paths resolve from the input file's directory. +- Ensure a browser context is available (screenshots require a browser). +- Verify the output directory exists or is writable. + +### Comments visible in rendered docs + +If test comments appear in your rendered documentation: + +- Use comment syntaxes that your renderer hides (such as JSX comments `{/* */}` for MDX). +- Avoid `[comment]: #` syntax if your renderer doesn't strip it. diff --git a/fern/pages/docs/tests/overview.mdx b/fern/pages/docs/tests/overview.mdx index 3760681..337bb33 100644 --- a/fern/pages/docs/tests/overview.mdx +++ b/fern/pages/docs/tests/overview.mdx @@ -5,7 +5,7 @@ description: Tests tell Doc Detective what actions to perform, how, and where. Tests tell Doc Detective what actions to perform, how, and where. Tests are made of three sets of components: -- **Test specification**: The highest-level component, test specifications (or specs) are collection of tests that should run together. [Contexts](/docs/configuration/contexts) defined in the spec are shared by all tests in the spec. Test specifications are equivalent to test suites in other testing frameworks. +- **Test specification**: The highest-level component, test specifications (or specs) are collection of tests that should run together. [Contexts](/docs/configuration#contexts) defined in the spec are shared by all tests in the spec. Test specifications are equivalent to test suites in other testing frameworks. - **Test**: A test to run within a spec. Each test has a name, and a set of steps to perform. Tests are equivalent to test cases in other testing frameworks. - **Steps**: A step is a single action to perform within a test. Each individual step acts as an assertion that the step completes as expected. Steps are roughly equivalent to assertions in other testing frameworks. @@ -38,7 +38,7 @@ When Doc Detective runs a test, the result is one of four statuses: ### Skipped tests -When Doc Detective runs tests, it checks if each [context](/docs/configuration/contexts) matches your current environment. If a context specifies a platform or browser that doesn't match the system running the test, Doc Detective skips that context and its associated test. +When Doc Detective runs tests, it checks if each [context](/docs/configuration#contexts) matches your current environment. If a context specifies a platform or browser that doesn't match the system running the test, Doc Detective skips that context and its associated test. Doc Detective skips tests when: @@ -249,6 +249,144 @@ docker run -v .:/app docdetective/docdetective --input /app/inline-tests.md,http +## Common test patterns + +Here are some patterns you can use as starting points for common testing scenarios. + +### Login flow + +```json +{ + "tests": [ + { + "description": "Test login flow", + "steps": [ + { "goTo": "https://app.example.com/login" }, + { "find": "input[name='email']" }, + { "type": ["user@example.com"] }, + { "find": "input[name='password']" }, + { "type": ["password123"] }, + { "click": "button[type='submit']" }, + { "find": ".dashboard" } + ] + } + ] +} +``` + +### Form submission + +```json +{ + "tests": [ + { + "description": "Test contact form", + "steps": [ + { "goTo": "https://example.com/contact" }, + { "find": "input[name='name']" }, + { "type": ["Jane Doe"] }, + { "find": "input[name='email']" }, + { "type": ["jane@example.com"] }, + { "find": "textarea[name='message']" }, + { "type": ["Hello, this is a test message."] }, + { "click": "button[type='submit']" }, + { "find": ".success-message" } + ] + } + ] +} +``` + +### API CRUD operations + +```json +{ + "tests": [ + { + "description": "Test API endpoints", + "steps": [ + { + "httpRequest": { + "url": "https://api.example.com/items", + "method": "POST", + "request": { + "headers": { "Content-Type": "application/json" }, + "body": { "name": "Test item" } + }, + "statusCodes": [201] + } + }, + { + "httpRequest": { + "url": "https://api.example.com/items", + "method": "GET", + "statusCodes": [200] + } + } + ] + } + ] +} +``` + +### Working with variables + +Load environment variables and use them across steps: + +```json +{ + "tests": [ + { + "description": "Test with variables", + "steps": [ + { "loadVariables": ".env" }, + { "goTo": "$SITE_URL" }, + { + "httpRequest": { + "url": "$API_URL/status", + "method": "GET", + "request": { + "headers": { + "Authorization": "Bearer $API_TOKEN" + } + }, + "statusCodes": [200] + } + } + ] + } + ] +} +``` + +### Setup and cleanup + +Use `before` and `after` properties to run steps before and after a test. Each property accepts a path to a test specification file whose first test's steps run in the current browser context: + +```json +{ + "tests": [ + { + "description": "Test with setup and cleanup", + "before": "./setup.spec.json", + "steps": [ + { "find": "h1" }, + { "screenshot": "homepage.png" } + ], + "after": "./cleanup.spec.json" + } + ] +} +``` + +## Best practices + +- **Use descriptive IDs**: Give tests and steps meaningful identifiers so failures are easy to diagnose. +- **Add descriptions**: Include a `description` for each test explaining what it validates. +- **Use specific selectors**: Prefer unique CSS selectors or IDs over generic ones to avoid fragile tests. +- **Add wait steps**: Insert `wait` steps after actions that trigger page loads or animations to avoid timing issues. +- **Keep tests focused**: Each test should validate one specific workflow or feature. + ## Read the results Doc Detective outputs test results to a `testResults-.json` file in your `output` directory. You can also specify your output directory with the `--output` flag: diff --git a/fern/pages/reference/api/detect-and-resolve-tests.mdx b/fern/pages/reference/api/detect-and-resolve-tests.mdx new file mode 100644 index 0000000..5d85d54 --- /dev/null +++ b/fern/pages/reference/api/detect-and-resolve-tests.mdx @@ -0,0 +1,160 @@ +--- +title: detectAndResolveTests() +description: Detects and resolves tests based on the provided configuration. +--- + +## Signature + +```javascript +async detectAndResolveTests({ config }) → any +``` + +## Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `{ config }` | any | Yes | | + +## Return value + +Returns `any`. + +{/* CUSTOM CONTENT START */} + + + + + + + +## Examples + +### Basic usage + +```javascript +import { detectAndResolveTests } from "doc-detective"; + +const config = { + input: "./docs", +}; + +const resolvedTests = await detectAndResolveTests({ config }); +if (resolvedTests) { + console.log(`Found ${resolvedTests.specs.length} specs`); +} +``` + +### Pass to runTests + +The most common use case: detect and resolve tests once, then pass them to `runTests()`: + +```javascript +import { detectAndResolveTests, runTests } from "doc-detective"; + +const config = { + input: "./docs", + output: "./test-results", +}; + +const resolvedTests = await detectAndResolveTests({ config }); +if (resolvedTests) { + const results = await runTests(config, { resolvedTests }); + console.log(results.summary); +} +``` + +### Inspect before running + +```javascript +import { detectAndResolveTests, runTests } from "doc-detective"; + +const config = { + input: "./docs", +}; + +const resolvedTests = await detectAndResolveTests({ config }); +if (resolvedTests) { + // Inspect the resolved tests + for (const spec of resolvedTests.specs) { + console.log(`Spec: ${spec.description || spec.file}`); + for (const test of spec.tests) { + console.log(` Test: ${test.description}`); + console.log(` Contexts: ${test.contexts.length}`); + } + } + + // Then run + const results = await runTests(config, { resolvedTests }); +} +``` + +### Context resolution + +```javascript +import { detectAndResolveTests } from "doc-detective"; + +const config = { + input: "./docs", + runOn: [ + { + platforms: ["linux"], + browsers: { name: "chrome", headless: true }, + }, + ], +}; + +const resolvedTests = await detectAndResolveTests({ config }); +``` + +### CI/CD integration + +```javascript +import { detectAndResolveTests, runTests } from "doc-detective"; + +const config = { + input: "./docs", + output: "./test-results", + logLevel: "error", + runOn: [ + { + platforms: ["linux"], + browsers: { name: "chrome", headless: true }, + }, + ], +}; + +const resolvedTests = await detectAndResolveTests({ config }); +if (!resolvedTests) { + console.log("No tests found."); + process.exit(0); +} + +const results = await runTests(config, { resolvedTests }); +if (results.summary.tests.fail > 0) { + process.exit(1); +} +``` + +## When to use + +Use `detectAndResolveTests()` when you want to: + +- **Prepare once, run many**: Detect and resolve tests once, then pass the resolved tests to `runTests()` multiple times (for example, with different configurations). +- **Inspect test structure**: Examine which tests and contexts were detected before running. +- **Validate tests**: Check that your test files are properly structured without executing them. +- **Cache resolved tests**: Store the resolved tests for later execution. + +If you don't need to inspect or reuse the resolved tests, you can call `runTests()` directly, which handles detection and resolution internally. + +## Notes + +- Internally calls `detectTests()` followed by `resolveTests()`. +- Returns `null` (instead of throwing) if no tests are found, so always check the return value. +- Context resolution is environment-dependent. Results differ based on the current platform and available browsers. + + + + + + +{/* CUSTOM CONTENT END */} diff --git a/fern/pages/reference/api/detect-tests.mdx b/fern/pages/reference/api/detect-tests.mdx new file mode 100644 index 0000000..51c9666 --- /dev/null +++ b/fern/pages/reference/api/detect-tests.mdx @@ -0,0 +1,152 @@ +--- +title: detectTests() +description: Detects tests from files based on config. +--- + +## Signature + +```javascript +async detectTests({ config }) → any +``` + +## Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|--------------| +| `{ config }` | any | Yes | | + +## Return value + +Returns `any`. + +{/* CUSTOM CONTENT START */} + + + + + + + +## Examples + +### Basic usage + +```javascript +import { detectTests } from "doc-detective"; + +const config = { + input: "./docs", +}; + +const specs = await detectTests({ config }); +console.log(`Detected ${specs.length} specs`); +``` + +### Multiple sources + +```javascript +import { detectTests } from "doc-detective"; + +const config = { + input: ["./docs", "./guides", "./api-docs"], + recursive: true, +}; + +const specs = await detectTests({ config }); +for (const spec of specs) { + console.log(`Spec: ${spec.description || spec.file}`); + console.log(` Tests: ${spec.tests.length}`); +} +``` + +### JSON/YAML spec files + +Detect standalone spec files: + +```javascript +import { detectTests } from "doc-detective"; + +const config = { + input: "./tests", +}; + +const specs = await detectTests({ config }); +// specs include both standalone .spec.json files and inline tests +``` + +### Coverage analysis + +Use detection results to analyze test coverage across your documentation: + +```javascript +import { detectTests } from "doc-detective"; + +const config = { + input: "./docs", +}; + +const specs = await detectTests({ config }); + +let totalTests = 0; +let totalSteps = 0; + +for (const spec of specs) { + totalTests += spec.tests.length; + for (const test of spec.tests) { + totalSteps += test.steps.length; + } +} + +console.log(`Coverage: ${specs.length} specs, ${totalTests} tests, ${totalSteps} steps`); +``` + +### Custom file types + +Detect tests in non-standard file formats by configuring custom file types: + +```javascript +import { detectTests } from "doc-detective"; + +const config = { + input: "./docs", + fileTypes: [ + { + extensions: [".rst"], + testStartStatementOpen: ".. test:: ", + testStartStatementClose: "", + testEndStatement: ".. test-end::", + stepStatementOpen: ".. step:: ", + stepStatementClose: "", + }, + ], +}; + +const specs = await detectTests({ config }); +``` + +## Behavior + +When called, `detectTests()` performs the following steps: + +1. Validates and applies the configuration. +2. Resolves input paths to a list of files. +3. Identifies file types based on extensions and configuration. +4. Reads each file and parses its content. +5. Extracts standalone test specifications (`.spec.json`, `.spec.yaml` files). +6. Detects inline test declarations from comments and markup patterns. +7. Auto-detects steps if `detectSteps` is enabled. +8. Returns the collected specs. + +## Notes + +- Files are matched against file type definitions by extension. Unknown extensions are skipped. +- Inline test detection depends on the `testStartStatementOpen`, `testEndStatement`, and `stepStatementOpen` patterns defined for each file type. +- The `detectSteps` configuration option controls whether Doc Detective auto-generates steps from detected markup patterns (links, images, code blocks). +- The returned specs don't include context resolution. Use `resolveTests()` to match specs to available contexts. + + + + + + +{/* CUSTOM CONTENT END */} diff --git a/fern/pages/reference/api/get-runner.mdx b/fern/pages/reference/api/get-runner.mdx new file mode 100644 index 0000000..72cae33 --- /dev/null +++ b/fern/pages/reference/api/get-runner.mdx @@ -0,0 +1,188 @@ +--- +title: getRunner() +description: Creates and returns a Chrome WebDriver instance with an Appium server. +--- + +## Signature + +```javascript +async getRunner(options?) → any +``` + +## Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `options` | any | No | | + +## Return value + +Returns `any`. + +{/* CUSTOM CONTENT START */} + + + + + + + +## Examples + +### Basic usage + +```javascript +import { getRunner } from "doc-detective"; + +const { runner, cleanup, runStep } = await getRunner(); + +try { + await runStep({ goTo: "https://example.com" }); + await runStep({ find: "h1" }); + await runStep({ screenshot: "example.png" }); +} finally { + await cleanup(); +} +``` + +### Headless mode + +```javascript +import { getRunner } from "doc-detective"; + +const { runner, cleanup, runStep } = await getRunner({ + headless: true, + width: 1920, + height: 1080, +}); + +try { + await runStep({ goTo: "https://example.com" }); + await runStep({ screenshot: "homepage.png" }); +} finally { + await cleanup(); +} +``` + +### Run Doc Detective steps + +Use `runStep` to execute any Doc Detective action: + +```javascript +import { getRunner } from "doc-detective"; + +const { cleanup, runStep } = await getRunner({ + headless: false, + width: 1280, + height: 720, +}); + +try { + // Navigate + await runStep({ goTo: "https://example.com" }); + + // Interact + await runStep({ find: "input[name='search']" }); + await runStep({ type: ["doc detective", "$ENTER$"] }); + + // Wait and capture + await runStep({ wait: 2000 }); + await runStep({ screenshot: "search-results.png" }); +} finally { + await cleanup(); +} +``` + +### Custom viewport size + +```javascript +import { getRunner } from "doc-detective"; + +// Mobile viewport +const { cleanup, runStep } = await getRunner({ + width: 375, + height: 812, +}); + +try { + await runStep({ goTo: "https://example.com" }); + await runStep({ screenshot: "mobile-view.png" }); +} finally { + await cleanup(); +} +``` + +### Error handling + +```javascript +import { getRunner } from "doc-detective"; + +const { cleanup, runStep } = await getRunner(); + +try { + await runStep({ goTo: "https://example.com" }); + const result = await runStep({ find: ".nonexistent-element" }); + console.log("Step result:", result); +} catch (error) { + console.error("Step failed:", error.message); +} finally { + await cleanup(); +} +``` + +### Test framework integration + +Use `getRunner()` within a test framework like Mocha: + +```javascript +import { getRunner } from "doc-detective"; + +describe("Documentation tests", function () { + let cleanup, runStep; + + before(async function () { + const runner = await getRunner({ headless: true }); + cleanup = runner.cleanup; + runStep = runner.runStep; + }); + + after(async function () { + await cleanup(); + }); + + it("should load the homepage", async function () { + await runStep({ goTo: "https://example.com" }); + await runStep({ find: "h1" }); + }); + + it("should navigate to docs", async function () { + await runStep({ goTo: "https://example.com/docs" }); + await runStep({ find: ".sidebar" }); + }); +}); +``` + +## Behavior + +When called, `getRunner()` performs the following steps: + +1. Starts an Appium server on an available port. +2. Configures Chrome WebDriver capabilities. +3. Sets viewport dimensions based on `width` and `height` options. +4. Applies headless mode if configured. +5. Creates a WebDriver session connected to Chrome via Appium. +6. Returns the runner, Appium process, cleanup function, and `runStep` function. + +## Notes + +- **Chrome only**: `getRunner()` currently supports Chrome exclusively. Use `runTests()` with context configuration for Firefox or WebKit. +- **Always call `cleanup()`**: Failing to call `cleanup()` leaves browser and Appium processes running. Use `try`/`finally` blocks. +- Set `headless` to `false` if you need to use the `record` action for video capture. +- The `runStep` function accepts the same step objects used in test specification files. + + + + + + +{/* CUSTOM CONTENT END */} diff --git a/fern/pages/reference/api/overview.mdx b/fern/pages/reference/api/overview.mdx new file mode 100644 index 0000000..ded7885 --- /dev/null +++ b/fern/pages/reference/api/overview.mdx @@ -0,0 +1,105 @@ +--- +title: Programmatic API overview +sidebar-title: Overview +description: Use Doc Detective's Node.js API to run tests, detect test specs, and manage test execution programmatically. +--- + +Doc Detective provides a Node.js API that you can use to integrate documentation testing into your own tools, scripts, and workflows. + +## Installation + +Install Doc Detective as a dependency in your project: + +```bash +npm install doc-detective +``` + +## Functions + +The API provides the following functions, grouped by category: + +### Test execution + +- [`runTests()`](/reference/api/run-tests): Run documentation tests and return results. +- [`getRunner()`](/reference/api/get-runner): Create a browser runner for executing individual test steps. + +### Test discovery and resolution + +- [`detectTests()`](/reference/api/detect-tests): Scan input files and detect test specifications. +- [`resolveTests()`](/reference/api/resolve-tests): Resolve detected tests against available contexts (platforms and browsers). +- [`detectAndResolveTests()`](/reference/api/detect-and-resolve-tests): Convenience function that combines `detectTests()` and `resolveTests()`. + +### File and path utilities + +- [`readFile()`](/reference/api/read-file): Read and parse local or remote files. +- [`resolvePaths()`](/reference/api/resolve-paths): Resolve relative paths in configuration and spec objects to absolute paths. + +## Basic usage + +### Run tests + +```javascript +import { runTests } from "doc-detective"; + +const config = { + input: "./docs", + output: "./test-results", +}; + +const results = await runTests(config); +console.log(results.summary); +``` + +### Detect and resolve tests + +```javascript +import { detectTests, resolveTests } from "doc-detective"; + +const config = { + input: "./docs", +}; + +// Detect test specs from input files +const detectedTests = await detectTests({ config }); + +// Resolve tests against available contexts +const resolvedTests = await resolveTests({ config, detectedTests }); + +// Inspect resolved tests before running +console.log(`Found ${resolvedTests.specs.length} specs`); +``` + +### Convenience workflow + +```javascript +import { detectAndResolveTests, runTests } from "doc-detective"; + +const config = { + input: "./docs", +}; + +// Detect and resolve in one step +const resolvedTests = await detectAndResolveTests({ config }); + +if (resolvedTests) { + // Run with pre-resolved tests + const results = await runTests(config, { resolvedTests }); + console.log(results.summary); +} +``` + +## Configuration + +All API functions accept a configuration object with the same options available in the [configuration file](/docs/configuration). See the [`config`](/reference/schemas/config) schema for all available options. + +## Error handling + +API functions handle errors gracefully: + +- `runTests()` returns `null` if no tests are found or an error occurs. +- `detectTests()` returns an empty array if no tests are detected. +- `resolveTests()` returns a `ResolvedTests` object with empty arrays if resolution finds no matches. +- `detectAndResolveTests()` returns `null` if no tests are found. +- `readFile()` returns `null` if the file can't be read or parsed. + +For detailed error information, set `logLevel` to `"debug"` in your configuration. diff --git a/fern/pages/reference/api/read-file.mdx b/fern/pages/reference/api/read-file.mdx new file mode 100644 index 0000000..ac416d1 --- /dev/null +++ b/fern/pages/reference/api/read-file.mdx @@ -0,0 +1,134 @@ +--- +title: readFile() +description: Reads and parses content from a remote URL or local file path, supporting JSON and YAML formats. +--- + +## Signature + +```javascript +async readFile({ fileURLOrPath }) → any +``` + +## Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|--------------| +| `{ fileURLOrPath }` | string | Yes | | + +## Return value + +Returns `any`. + +{/* CUSTOM CONTENT START */} + + + + + + + +## Examples + +### Read a JSON file + +```javascript +import { readFile } from "doc-detective"; + +const config = await readFile({ fileURLOrPath: "./.doc-detective.json" }); +if (config) { + console.log("Config:", config); + // config is a parsed JavaScript object +} +``` + +### Read a YAML file + +```javascript +import { readFile } from "doc-detective"; + +const config = await readFile({ fileURLOrPath: "./.doc-detective.yaml" }); +if (config) { + console.log("Config:", config); + // config is a parsed JavaScript object +} +``` + +### Read a remote file + +```javascript +import { readFile } from "doc-detective"; + +const content = await readFile({ + fileURLOrPath: "https://example.com/tests/sample.spec.json", +}); +if (content) { + console.log("Remote spec:", content); +} +``` + +### Read a text file + +```javascript +import { readFile } from "doc-detective"; + +const markdown = await readFile({ fileURLOrPath: "./docs/README.md" }); +if (markdown) { + console.log("File contents:", markdown); + // markdown is a raw string +} +``` + +### Error handling + +```javascript +import { readFile } from "doc-detective"; + +const content = await readFile({ fileURLOrPath: "./nonexistent-file.json" }); +if (content === null) { + console.log("File not found or could not be read."); +} +``` + +### Read multiple files + +```javascript +import { readFile } from "doc-detective"; + +const files = ["./config.json", "./tests/spec1.json", "./tests/spec2.yaml"]; + +for (const file of files) { + const content = await readFile({ fileURLOrPath: file }); + if (content) { + console.log(`${file}:`, typeof content === "object" ? "parsed" : "raw"); + } else { + console.log(`${file}: could not be read`); + } +} +``` + +## Behavior + +When called, `readFile()` performs the following steps: + +1. Determines if the input is a URL or local file path. +2. For URLs, fetches the content using an HTTP GET request. +3. For local paths, reads the file from the file system. +4. Checks the file extension to determine the content type. +5. If the file is JSON, parses it with `JSON.parse()`. +6. If the file is YAML, parses it with a YAML parser. +7. For all other file types, returns the raw string content. +8. Returns `null` if any step fails (file not found, parse error, network error). + +## Notes + +- `readFile()` returns `null` on errors rather than throwing exceptions. Always check the return value. +- Both absolute and relative file paths are supported. Relative paths resolve from the current working directory. +- Remote URLs are fetched using HTTP GET requests. +- If a JSON or YAML file contains invalid syntax, `readFile()` returns `null`. + + + + + + +{/* CUSTOM CONTENT END */} diff --git a/fern/pages/reference/api/resolve-paths.mdx b/fern/pages/reference/api/resolve-paths.mdx new file mode 100644 index 0000000..7b824f8 --- /dev/null +++ b/fern/pages/reference/api/resolve-paths.mdx @@ -0,0 +1,149 @@ +--- +title: resolvePaths() +description: Convert recognized relative path properties in a config or spec object to absolute paths. +--- + +## Signature + +```javascript +async resolvePaths({ config, object, filePath, nested?, objectType? }) → any +``` + +## Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `config` | any | Yes | | +| `object` | any | Yes | | +| `filePath` | string | Yes | | +| `nested` | boolean | No | | +| `objectType` | string | No | | + +## Return value + +Returns `any`. + +{/* CUSTOM CONTENT START */} + + + + + + + +## Examples + +### Resolve config paths + +```javascript +import { resolvePaths } from "doc-detective"; + +const config = { + input: "./docs", + output: "./test-results", + relativePathBase: "file", +}; + +const resolved = await resolvePaths({ + config, + object: config, + filePath: "/home/user/project/.doc-detective.json", + objectType: "config", +}); + +// resolved.input is now "/home/user/project/docs" +// resolved.output is now "/home/user/project/test-results" +``` + +### Resolve spec paths + +```javascript +import { resolvePaths } from "doc-detective"; + +const config = { + relativePathBase: "file", +}; + +const spec = { + tests: [ + { + steps: [ + { screenshot: "./screenshots/home.png" }, + ], + }, + ], +}; + +const resolved = await resolvePaths({ + config, + object: spec, + filePath: "/home/user/project/tests/login.spec.json", + objectType: "spec", +}); +``` + +### File-based vs. CWD-based resolution + +When `relativePathBase` is `"file"`, paths resolve relative to the file containing them. When set to `"cwd"`, paths resolve relative to the current working directory. + +```javascript +import { resolvePaths } from "doc-detective"; + +// File-based resolution +const fileBasedConfig = { + input: "./docs", + relativePathBase: "file", +}; + +await resolvePaths({ + config: fileBasedConfig, + object: fileBasedConfig, + filePath: "/home/user/project/.doc-detective.json", +}); +// input → "/home/user/project/docs" + +// CWD-based resolution +const cwdBasedConfig = { + input: "./docs", + relativePathBase: "cwd", +}; + +await resolvePaths({ + config: cwdBasedConfig, + object: cwdBasedConfig, + filePath: "/home/user/project/.doc-detective.json", +}); +// input → "{cwd}/docs" (relative to current working directory) +``` + +## Behavior + +When called, `resolvePaths()`: + +1. Reads the `relativePathBase` setting from the config (`"file"` or `"cwd"`). +2. Determines the base directory from the `filePath` parameter. +3. Identifies path properties in the object based on the object type. +4. For each relative path, resolves it to an absolute path using the appropriate base. +5. Preserves absolute paths and URLs (doesn't modify them). +6. Processes nested objects if `nested` is `true`. +7. Returns the modified object. + +### Recognized path properties + +For **config** objects: `input`, `output`, and paths within `fileTypes`, `beforeAny`, and `afterAll`. + +For **spec** objects: paths within test steps, such as screenshot paths, file paths in `loadVariables`, `runShell`, `runCode`, and other actions. + +## Notes + +- Absolute paths and URLs are preserved without modification. +- The function modifies the object in place and returns it. +- If `objectType` is not specified, the function auto-detects whether the object is a config or spec based on its properties. +- Set `nested` to `false` to only resolve top-level paths in the object. + + + + + + +{/* CUSTOM CONTENT END */} diff --git a/fern/pages/reference/api/resolve-tests.mdx b/fern/pages/reference/api/resolve-tests.mdx new file mode 100644 index 0000000..38a9f5c --- /dev/null +++ b/fern/pages/reference/api/resolve-tests.mdx @@ -0,0 +1,156 @@ +--- +title: resolveTests() +description: Resolves detected tests into fully-resolved test specifications. +--- + +## Signature + +```javascript +async resolveTests({ config, detectedTests }) → any +``` + +## Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `config` | any | Yes | | +| `detectedTests` | any[] | Yes | | + +## Return value + +Returns `any`. + +{/* CUSTOM CONTENT START */} + + + + + + + +## Examples + +### Basic usage + +```javascript +import { detectTests, resolveTests } from "doc-detective"; + +const config = { + input: "./docs", +}; + +const detectedTests = await detectTests({ config }); +const resolvedTests = await resolveTests({ config, detectedTests }); + +console.log(`Resolved ${resolvedTests.specs.length} specs`); +``` + +### Context resolution + +Resolve tests against specific contexts: + +```javascript +import { detectTests, resolveTests } from "doc-detective"; + +const config = { + input: "./docs", + runOn: [ + { + platforms: ["linux"], + browsers: { name: "chrome", headless: true }, + }, + { + platforms: ["linux"], + browsers: { name: "firefox", headless: true }, + }, + ], +}; + +const detectedTests = await detectTests({ config }); +const resolvedTests = await resolveTests({ config, detectedTests }); +``` + +### Inspect contexts before running + +```javascript +import { detectTests, resolveTests } from "doc-detective"; + +const config = { + input: "./docs", +}; + +const detectedTests = await detectTests({ config }); +const resolvedTests = await resolveTests({ config, detectedTests }); + +for (const spec of resolvedTests.specs) { + for (const test of spec.tests) { + console.log(`Test: ${test.description}`); + for (const context of test.contexts) { + console.log(` Context: ${context.platform} / ${context.browser?.name}`); + } + } +} +``` + +### Pass to runTests + +```javascript +import { detectTests, resolveTests, runTests } from "doc-detective"; + +const config = { + input: "./docs", +}; + +const detectedTests = await detectTests({ config }); +const resolvedTests = await resolveTests({ config, detectedTests }); + +// Run with pre-resolved tests +const results = await runTests(config, { resolvedTests }); +``` + +### Multi-platform resolution + +```javascript +import { detectTests, resolveTests } from "doc-detective"; + +const config = { + input: "./docs", + runOn: [ + { platforms: ["windows", "mac", "linux"], browsers: "chrome" }, + { platforms: ["windows", "mac", "linux"], browsers: "firefox" }, + { platforms: "mac", browsers: "webkit" }, + ], +}; + +const detectedTests = await detectTests({ config }); +const resolvedTests = await resolveTests({ config, detectedTests }); + +// Only contexts matching the current platform and available browsers are included +``` + +## Behavior + +When called, `resolveTests()` performs the following steps: + +1. Validates the configuration and detected tests. +2. Evaluates each context's platform against the current operating system. +3. Checks browser availability for each context. +4. Assigns matching contexts to each test based on precedence (test > spec > config). +5. Detects browser requirements from test steps (for example, `goTo` or `find` actions need a browser). +6. Marks contexts as skippable when the platform or browser doesn't match. +7. Resolves relative paths in specs. +8. Returns the fully resolved test structure. + +## Notes + +- Context resolution is environment-dependent. The same specs resolve differently on different platforms. +- Tests that don't require a browser (for example, tests with only `checkLink` or `httpRequest` steps) may resolve without a browser context. +- If a test defines its own `runOn` contexts, those override spec- and config-level contexts. +- Use the resolved output to inspect which tests will actually run before calling `runTests()`. + + + + + + +{/* CUSTOM CONTENT END */} diff --git a/fern/pages/reference/api/run-tests.mdx b/fern/pages/reference/api/run-tests.mdx new file mode 100644 index 0000000..2401b28 --- /dev/null +++ b/fern/pages/reference/api/run-tests.mdx @@ -0,0 +1,171 @@ +--- +title: runTests() +description: Runs detected documentation tests and returns aggregated results. +--- + +## Signature + +```javascript +async runTests(config, options?) → any +``` + +## Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `config` | any | Yes | | +| `options` | any | No | | + +## Return value + +Returns `any`. + +{/* CUSTOM CONTENT START */} + + + + + + + +## Examples + +### Basic usage + +```javascript +import { runTests } from "doc-detective"; + +const config = { + input: "./docs", + output: "./test-results", +}; + +const results = await runTests(config); +if (results) { + console.log("Tests completed:", results.summary); +} else { + console.log("No tests found."); +} +``` + +### With pre-resolved tests + +Use pre-resolved tests to separate the detection/resolution phase from execution. This is useful when you want to inspect or filter tests before running them. + +```javascript +import { detectAndResolveTests, runTests } from "doc-detective"; + +const config = { + input: "./docs", +}; + +const resolvedTests = await detectAndResolveTests({ config }); + +if (resolvedTests) { + const results = await runTests(config, { resolvedTests }); + console.log(results.summary); +} +``` + +### Handle results + +```javascript +import { runTests } from "doc-detective"; + +const config = { + input: "./docs", +}; + +const results = await runTests(config); +if (results) { + const { pass, fail, warning, skipped } = results.summary.tests; + console.log(`Pass: ${pass}, Fail: ${fail}, Warning: ${warning}, Skipped: ${skipped}`); + + if (fail > 0) { + process.exit(1); + } +} +``` + +### Multiple inputs + +```javascript +import { runTests } from "doc-detective"; + +const config = { + input: ["./docs", "./guides", "./tutorials"], + output: "./test-results", +}; + +const results = await runTests(config); +``` + +### Specific contexts + +```javascript +import { runTests } from "doc-detective"; + +const config = { + input: "./docs", + runOn: [ + { + platforms: ["linux"], + browsers: { + name: "chrome", + headless: true, + }, + }, + ], +}; + +const results = await runTests(config); +``` + +### CI/CD integration + +```javascript +import { runTests } from "doc-detective"; + +const config = { + input: "./docs", + output: "./test-results", + logLevel: "error", + runOn: [ + { + platforms: ["linux"], + browsers: { name: "chrome", headless: true }, + }, + ], +}; + +const results = await runTests(config); +if (!results || results.summary.tests.fail > 0) { + process.exit(1); +} +``` + +## Behavior + +When called, `runTests()` performs the following steps: + +1. Validates and applies the configuration. +2. Detects test specifications from input files (unless `resolvedTests` is provided). +3. Resolves tests against available contexts. +4. Launches browser runners for each context. +5. Executes test steps in sequence within each test. +6. Collects results for each step, test, and context. +7. Cleans up browser sessions. +8. Returns the aggregated results. + +## Notes + +- If `resolvedTests` is provided, steps 2 and 3 are skipped, which can improve performance when running the same tests multiple times. +- Set `concurrentRunners` in the configuration to run tests in parallel across contexts. +- The function returns `null` rather than throwing errors, so always check the return value. + + + + + + +{/* CUSTOM CONTENT END */} diff --git a/fern/pages/reference/glossary.mdx b/fern/pages/reference/glossary.mdx index 01c29c3..1442d82 100644 --- a/fern/pages/reference/glossary.mdx +++ b/fern/pages/reference/glossary.mdx @@ -15,7 +15,7 @@ A lightweight markup language for technical documentation. Doc Detective support ## Context -A combination of platform (operating system) and browser where tests run. Contexts can be defined at the config, spec, or test level to control where tests execute. See [Contexts](/docs/configuration/contexts). +A combination of platform (operating system) and browser where tests run. Contexts can be defined at the config, spec, or test level to control where tests execute. See [Contexts](/docs/configuration#contexts). ## CSS selector diff --git a/fern/pages/reference/schemas/browser.md b/fern/pages/reference/schemas/browser.md index 2be9085..05d177b 100644 --- a/fern/pages/reference/schemas/browser.md +++ b/fern/pages/reference/schemas/browser.md @@ -24,13 +24,7 @@ viewport | object([Browser Viewport](/reference/schemas/browser-viewport)) | Opt { "name": "chrome", "headless": true, - "window": { - "width": 42, - "height": 42 - }, - "viewport": { - "width": 42, - "height": 42 - } + "window": {}, + "viewport": {} } ``` diff --git a/fern/pages/reference/schemas/capture-screenshot-detailed.md b/fern/pages/reference/schemas/capture-screenshot-detailed.md index 3ad5ab0..617acd0 100644 --- a/fern/pages/reference/schemas/capture-screenshot-detailed.md +++ b/fern/pages/reference/schemas/capture-screenshot-detailed.md @@ -16,6 +16,7 @@ maxVariation | number | Optional. Allowed variation in percentage of pixels betw overwrite | string | Optional. If `true`, overwrites the existing screenshot at `path` if it exists. If `aboveVariation`, overwrites the existing screenshot at `path` if the difference between the new screenshot and the existing screenshot is greater than `maxVariation`.

Accepted values: `true`, `false`, `aboveVariation` | `aboveVariation` crop | one of:
- string
- object([Crop by element (detailed)](/reference/schemas/crop-by-element-detailed)) | Optional. No description provided. | +sourceIntegration | object(sourceIntegration) | Optional. Information about the source integration for this screenshot, enabling upload of changed files back to the source CMS. Set automatically during test resolution for files from integrations. | ## Examples @@ -25,6 +26,12 @@ crop | one of:
- string
- object([Crop by element (detailed)](/reference "directory": "example", "maxVariation": 0.05, "overwrite": "aboveVariation", - "crop": "example" + "crop": "example", + "sourceIntegration": { + "type": "heretto", + "integrationName": "my-heretto", + "fileId": "8f3ed200-cbba-11e1-ac51-c82a1446d15c", + "filePath": "/db/organizations/example/repositories/docs/images/screenshot.png" + } } ``` diff --git a/fern/pages/reference/schemas/check-link-detailed.md b/fern/pages/reference/schemas/check-link-detailed.md index 6ab8ce3..40d436a 100644 --- a/fern/pages/reference/schemas/check-link-detailed.md +++ b/fern/pages/reference/schemas/check-link-detailed.md @@ -12,7 +12,7 @@ Check if an HTTP or HTTPS URL returns an acceptable status code from a GET reque Field | Type | Description | Default :-- | :-- | :-- | :-- -url | string | Required. URL to check. Can be a full URL or a path. If a path is provided, `origin` must be specified.

Pattern: `(^(http://|https://|/).*|\$[A-Za-z0-9_]+)` | +url | string | Required. URL to check. Can be a full URL or a path. If a path is provided, `origin` must be specified.

Pattern: `(^(http://|https://|/).*$|^\$[A-Za-z0-9_]+$)` | origin | string | Optional. Protocol and domain to navigate to. Prepended to `url`. | statusCodes | one of:
- integer
- array of integer | Optional. Accepted status codes. If the specified URL returns a code other than what is specified here, the action fails. | ``[200,301,302,307,308]`` diff --git a/fern/pages/reference/schemas/checklink.md b/fern/pages/reference/schemas/checklink.md index 8e1a427..5c35053 100644 --- a/fern/pages/reference/schemas/checklink.md +++ b/fern/pages/reference/schemas/checklink.md @@ -5,8 +5,8 @@ title: "checkLink" ## Referenced In - [Markup definition](/reference/schemas/markup-definition) -- [test](/reference/schemas/test) - [Resolved context](/reference/schemas/resolved-context) +- [test](/reference/schemas/test) ## Fields @@ -18,6 +18,33 @@ checkLink | one of:
- string
- object([Check link (detailed)](/reference ```json { - "checkLink": "example" + "checkLink": "https://www.google.com" +} +``` + +```json +{ + "checkLink": "/search" +} +``` + +```json +{ + "checkLink": { + "url": "https://www.google.com", + "statusCodes": 200 + } +} +``` + +```json +{ + "checkLink": { + "url": "/search", + "origin": "https://www.google.com", + "statusCodes": [ + 200 + ] + } } ``` diff --git a/fern/pages/reference/schemas/click-element-detailed.md b/fern/pages/reference/schemas/click-element-detailed.md index 9b184ec..0fd2fef 100644 --- a/fern/pages/reference/schemas/click-element-detailed.md +++ b/fern/pages/reference/schemas/click-element-detailed.md @@ -12,8 +12,13 @@ title: "Click element (detailed)" Field | Type | Description | Default :-- | :-- | :-- | :-- button | string | Optional. Kind of click to perform.

Accepted values: `left`, `right`, `middle` | -elementText | string | Optional. Display text of the element to click. If combined with `selector`, the element must match both the text and the selector. | -selector | string | Optional. Selector of the element to click. If combined with `elementText`, the element must match both the text and the selector. | +elementText | string | Optional. Display text of the element to click. If combined with other element finding fields, the element must match all specified criteria. | +selector | string | Optional. Selector of the element to click. If combined with other element finding fields, the element must match all specified criteria. | +elementId | string | Optional. ID attribute of the element to click. Supports exact match or regex pattern using /pattern/ syntax. | +elementTestId | string | Optional. data-testid attribute of the element to click. Supports exact match or regex pattern using /pattern/ syntax. | +elementClass | one of:
- string
- array of string | Optional. Class or array of classes that the element must have. Each class supports exact match or regex pattern using /pattern/ syntax. Element must have all specified classes. | +elementAttribute | object | Optional. Object of attribute key-value pairs that the element must have. Values can be strings (supporting /pattern/ regex), numbers, or booleans. Boolean true matches attribute presence, false matches absence. | +elementAria | string | Optional. Computed accessible name of the element per ARIA specification. Supports exact match or regex pattern using /pattern/ syntax. | ## Examples @@ -21,6 +26,11 @@ selector | string | Optional. Selector of the element to click. If combined with { "button": "left", "elementText": "example", - "selector": "example" + "selector": "example", + "elementId": "example", + "elementTestId": "example", + "elementClass": "example", + "elementAttribute": {}, + "elementAria": "example" } ``` diff --git a/fern/pages/reference/schemas/click.md b/fern/pages/reference/schemas/click.md index 63d1f7c..f18b27e 100644 --- a/fern/pages/reference/schemas/click.md +++ b/fern/pages/reference/schemas/click.md @@ -4,9 +4,10 @@ title: "click" ## Referenced In +- [Find element (detailed)](/reference/schemas/find-element-detailed) - [Markup definition](/reference/schemas/markup-definition) -- [test](/reference/schemas/test) - [Resolved context](/reference/schemas/resolved-context) +- [test](/reference/schemas/test) ## Fields @@ -18,6 +19,31 @@ click | one of:
- string
- object([Click element (detailed)](/reference/ ```json { - "click": "example" + "click": true +} +``` + +```json +{ + "click": "Submit" +} +``` + +```json +{ + "click": { + "button": "left", + "elementText": "Element text" + } +} +``` + +```json +{ + "click": { + "selector": "#elementToScreenshot", + "elementText": "Element text", + "button": "middle" + } } ``` diff --git a/fern/pages/reference/schemas/common.md b/fern/pages/reference/schemas/common.md index 11826f1..556efec 100644 --- a/fern/pages/reference/schemas/common.md +++ b/fern/pages/reference/schemas/common.md @@ -2,12 +2,6 @@ title: "Common" --- -## Referenced In - -- [Markup definition](/reference/schemas/markup-definition) -- [test](/reference/schemas/test) -- [Resolved context](/reference/schemas/resolved-context) - ## Fields Field | Type | Description | Default diff --git a/fern/pages/reference/schemas/config.md b/fern/pages/reference/schemas/config.md index 657fd18..151e5e3 100644 --- a/fern/pages/reference/schemas/config.md +++ b/fern/pages/reference/schemas/config.md @@ -25,7 +25,7 @@ crawl | boolean | Optional. If `true`, crawls sitemap.xml files specified by URL processDitaMaps | boolean | Optional. If `true`, processes DITA maps and includes generated files as inputs. | `true` logLevel | string | Optional. Amount of detail to output when performing an operation.

Accepted values: `silent`, `error`, `warning`, `info`, `debug` | `info` runOn | array of object([context](/reference/schemas/context)) | Optional. Contexts to run the test in. Overrides contexts defined at the config and spec levels. | -fileTypes | array of one of: string, object([File type (custom)](/reference/schemas/file-type-custom)), object([File type (executable)](/reference/schemas/file-type-executable)) | Optional. Configuration for file types and their markup detection. | ``["markdown","asciidoc","html","dita"]`` +fileTypes | array of one of:
- string
- object([File type (custom)](/reference/schemas/file-type-custom))
- object([File type (executable)](/reference/schemas/file-type-executable)) | Optional. Configuration for file types and their markup detection. | ``["markdown","asciidoc","html","dita"]`` integrations | object([Integrations options](/reference/schemas/integrations-options)) | Optional. Options for connecting to external services. | telemetry | object([Telemetry options](/reference/schemas/telemetry-options)) | Optional. Options around sending telemetry for Doc Detective usage. | ``{"send":true}`` concurrentRunners | integer,boolean | Optional. Number of concurrent test runners. Set to true to use CPU core count (capped at 4).

Minimum: 1 | `1` @@ -175,3 +175,24 @@ debug | one of:
- boolean
- string | Optional. Enable debugging mode. `t "crawl": true } ``` + +```json +{ + "processDitaMaps": true +} +``` + +```json +{ + "integrations": { + "heretto": [ + { + "name": "example", + "organizationId": "your-organization-id", + "username": "your-username", + "apiToken": "your-api-token" + } + ] + } +} +``` diff --git a/fern/pages/reference/schemas/context.md b/fern/pages/reference/schemas/context.md index ef0b126..35d14f1 100644 --- a/fern/pages/reference/schemas/context.md +++ b/fern/pages/reference/schemas/context.md @@ -17,7 +17,7 @@ Field | Type | Description | Default $schema | string | Optional. JSON Schema for this object.

Accepted values: `https://raw.githubusercontent.com/doc-detective/common/refs/heads/main/dist/schemas/context_v3.schema.json` | contextId | string | Optional. Unique identifier for the context. | platforms | one of:
- string
- array of string | Optional. Platforms to run tests on. | -browsers | one of:
- string
- object([Browser](/reference/schemas/browser))
- array of one of: string, object([Browser](/reference/schemas/browser)) | Optional. Browsers to run tests on. | +browsers | one of:
- string
- object([Browser](/reference/schemas/browser))
- array of one of:
- string
- object([Browser](/reference/schemas/browser)) | Optional. Browsers to run tests on. | ## Examples diff --git a/fern/pages/reference/schemas/crop-by-element-detailed.md b/fern/pages/reference/schemas/crop-by-element-detailed.md index 132b590..c75df99 100644 --- a/fern/pages/reference/schemas/crop-by-element-detailed.md +++ b/fern/pages/reference/schemas/crop-by-element-detailed.md @@ -14,6 +14,11 @@ Field | Type | Description | Default :-- | :-- | :-- | :-- elementText | string | Optional. Display text of the element to screenshot. | selector | string | Optional. Selector of the element to screenshot. | +elementId | string | Optional. ID attribute of the element to find. Supports exact match or regex pattern using /pattern/ syntax. | +elementTestId | string | Optional. data-testid attribute of the element to find. Supports exact match or regex pattern using /pattern/ syntax. | +elementClass | one of:
- string
- array of string | Optional. Class or array of classes that the element must have. Each class supports exact match or regex pattern using /pattern/ syntax. Element must have all specified classes. | +elementAttribute | object | Optional. Object of attribute key-value pairs that the element must have. Values can be strings (supporting /pattern/ regex), numbers, or booleans. Boolean true matches attribute presence, false matches absence. | +elementAria | string | Optional. Computed accessible name of the element per ARIA specification. Supports exact match or regex pattern using /pattern/ syntax. | padding | one of:
- number
- object([Padding (detailed)](/reference/schemas/padding-detailed)) | Optional. No description provided. | ## Examples @@ -22,6 +27,11 @@ padding | one of:
- number
- object([Padding (detailed)](/reference/sche { "elementText": "example", "selector": "example", + "elementId": "example", + "elementTestId": "example", + "elementClass": "example", + "elementAttribute": {}, + "elementAria": "example", "padding": 42 } ``` diff --git a/fern/pages/reference/schemas/draganddrop.md b/fern/pages/reference/schemas/draganddrop.md index 393f78b..9afdc67 100644 --- a/fern/pages/reference/schemas/draganddrop.md +++ b/fern/pages/reference/schemas/draganddrop.md @@ -5,8 +5,8 @@ title: "dragAndDrop" ## Referenced In - [Markup definition](/reference/schemas/markup-definition) -- [test](/reference/schemas/test) - [Resolved context](/reference/schemas/resolved-context) +- [test](/reference/schemas/test) ## Fields @@ -18,6 +18,72 @@ dragAndDrop | object([dragAndDrop](/reference/schemas/draganddrop)) | Required. ```json { - "dragAndDrop": {} + "dragAndDrop": { + "source": "Table", + "target": "#canvas" + } +} +``` + +```json +{ + "dragAndDrop": { + "source": ".draggable-block", + "target": ".drop-zone", + "duration": 2000 + } +} +``` + +```json +{ + "dragAndDrop": { + "source": { + "selector": ".widget", + "elementText": "Data Table" + }, + "target": { + "selector": "#design-canvas" + }, + "duration": 500 + } +} +``` + +```json +{ + "dragAndDrop": { + "source": { + "selector": ".draggable", + "timeout": 10000 + }, + "target": { + "elementText": "Drop Zone", + "timeout": 5000 + } + } +} +``` + +```json +{ + "dragAndDrop": { + "source": "/Widget Item.*/", + "target": "#canvas" + } +} +``` + +```json +{ + "dragAndDrop": { + "source": { + "selector": ".draggable", + "elementText": "/Button [0-9]+/" + }, + "target": { + "elementText": "/Drop Zone.*/" + } + } } ``` diff --git a/fern/pages/reference/schemas/file-type-custom.md b/fern/pages/reference/schemas/file-type-custom.md index caf7ebe..6cbe9ae 100644 --- a/fern/pages/reference/schemas/file-type-custom.md +++ b/fern/pages/reference/schemas/file-type-custom.md @@ -22,6 +22,7 @@ markup | array of object([Markup definition](/reference/schemas/markup-definitio { "name": "example", "extends": "markdown", + "extensions": "example", "inlineStatements": {}, "markup": [] } diff --git a/fern/pages/reference/schemas/file-type-executable.md b/fern/pages/reference/schemas/file-type-executable.md index 6aef6b3..a5d94c7 100644 --- a/fern/pages/reference/schemas/file-type-executable.md +++ b/fern/pages/reference/schemas/file-type-executable.md @@ -14,3 +14,10 @@ extensions | one of:
- string
- array of string | Required. File extensi runShell | one of:
- string
- object([Run shell command (detailed)](/reference/schemas/run-shell-command-detailed)) | Optional. `runShell` step to perform for this file type. Use $1 as a placeholder for the file path. | ## Examples + +```json +{ + "extensions": "example", + "runShell": "docker run hello-world" +} +``` diff --git a/fern/pages/reference/schemas/find-element-detailed.md b/fern/pages/reference/schemas/find-element-detailed.md index b6f873e..4afd62f 100644 --- a/fern/pages/reference/schemas/find-element-detailed.md +++ b/fern/pages/reference/schemas/find-element-detailed.md @@ -10,12 +10,17 @@ title: "Find element (detailed)" Field | Type | Description | Default :-- | :-- | :-- | :-- -elementText | string | Optional. Display text of the element to find. If combined with `selector`, the element must match both the text and the selector. | -selector | string | Optional. Selector of the element to find. If combined with `elementText`, the element must match both the text and the selector. | +elementText | string | Optional. Display text of the element to find. If combined with other element finding fields, the element must match all specified criteria. | +selector | string | Optional. Selector of the element to find. If combined with other element finding fields, the element must match all specified criteria. | +elementId | string | Optional. ID attribute of the element to find. Supports exact match or regex pattern using /pattern/ syntax. | +elementTestId | string | Optional. data-testid attribute of the element to find. Supports exact match or regex pattern using /pattern/ syntax. | +elementClass | one of:
- string
- array of string | Optional. Class or array of classes that the element must have. Each class supports exact match or regex pattern using /pattern/ syntax. Element must have all specified classes. | +elementAttribute | object | Optional. Object of attribute key-value pairs that the element must have. Values can be strings (supporting /pattern/ regex), numbers, or booleans. Boolean true matches attribute presence, false matches absence. | +elementAria | string | Optional. Computed accessible name of the element per ARIA specification. Supports exact match or regex pattern using /pattern/ syntax. | timeout | integer | Optional. Max duration in milliseconds to wait for the element to exist. | `5000` moveTo | boolean | Optional. Move to the element. If the element isn't visible, it's scrolled into view. | `true` -click | one of:
- one of:
- string
- object([Click element (detailed)](/reference/schemas/click-element-detailed))
- boolean
- object([Find element and click](/reference/schemas/find-element-and-click)) | Optional. Click the element. | -type | unknown | Optional. Type keys after finding the element. Either a string or an object with a `keys` field as defined in [`type`](type). To type in the element, make the element active with the `click` parameter. | +click | one of:
- string
- object([Click element (detailed)](/reference/schemas/click-element-detailed))
- boolean
- object([Find element and click](/reference/schemas/find-element-and-click)) | Optional. Click the element. | +type | one of:
- string
- array of string
- object([Type keys (detailed)](/reference/schemas/type-keys-detailed)) | Optional. Type keys after finding the element. Either a string or an object with a `keys` field as defined in [`type`](type). To type in the element, make the element active with the `click` parameter. | ## Examples @@ -23,7 +28,14 @@ type | unknown | Optional. Type keys after finding the element. Either a string { "elementText": "example", "selector": "example", + "elementId": "example", + "elementTestId": "example", + "elementClass": "example", + "elementAttribute": {}, + "elementAria": "example", "timeout": 5000, - "moveTo": true + "moveTo": true, + "click": true, + "type": "example" } ``` diff --git a/fern/pages/reference/schemas/find.md b/fern/pages/reference/schemas/find.md index d052cfc..769446c 100644 --- a/fern/pages/reference/schemas/find.md +++ b/fern/pages/reference/schemas/find.md @@ -5,8 +5,8 @@ title: "find" ## Referenced In - [Markup definition](/reference/schemas/markup-definition) -- [test](/reference/schemas/test) - [Resolved context](/reference/schemas/resolved-context) +- [test](/reference/schemas/test) ## Fields @@ -18,6 +18,74 @@ find | one of:
- string
- object([Find element (detailed)](/reference/sc ```json { - "find": "example" + "find": "Find me!" +} +``` + +```json +{ + "find": { + "selector": "[title=Search]" + } +} +``` + +```json +{ + "find": { + "selector": "[title=Search]", + "timeout": 10000, + "elementText": "Search", + "moveTo": true, + "click": true, + "type": "shorthair cat" + } +} +``` + +```json +{ + "find": { + "selector": "[title=Search]", + "click": { + "button": "right" + } + } +} +``` + +```json +{ + "find": { + "selector": "[title=Search]", + "timeout": 10000, + "elementText": "Search", + "moveTo": true, + "click": true, + "type": { + "keys": [ + "shorthair cat" + ], + "inputDelay": 100 + } + } +} +``` + +```json +{ + "find": { + "elementId": "/^user-[0-9]+$/", + "elementClass": [ + "admin", + "/^level-[1-5]$/" + ], + "elementAttribute": { + "data-active": true, + "data-score": "/^[0-9]+$/" + }, + "timeout": 8000, + "moveTo": false + } } ``` diff --git a/fern/pages/reference/schemas/go-to-url-detailed.md b/fern/pages/reference/schemas/go-to-url-detailed.md index 1275ec0..7284e39 100644 --- a/fern/pages/reference/schemas/go-to-url-detailed.md +++ b/fern/pages/reference/schemas/go-to-url-detailed.md @@ -14,12 +14,16 @@ Field | Type | Description | Default :-- | :-- | :-- | :-- url | string | Required. URL to navigate to. Can be a full URL or a path. If a path is provided and `origin` is specified, prepends `origin` to `url`. If a path is provided but `origin` isn't specified, attempts to navigate relative to the current URL, if any.

Pattern: `(^(http://|https://|/).*|\$[A-Za-z0-9_]+)` | origin | string | Optional. Protocol and domain to navigate to. Prepended to `url`. | +timeout | integer | Optional. Maximum time in milliseconds to wait for the page to be ready. If exceeded, the goTo action fails.

Minimum: 0 | `30000` +waitUntil | object | Optional. Configuration for waiting conditions after navigation. | ## Examples ```json { - "url": "example", - "origin": "example" + "url": "/example", + "origin": "https://www.example.com", + "timeout": 30000, + "waitUntil": {} } ``` diff --git a/fern/pages/reference/schemas/goto.md b/fern/pages/reference/schemas/goto.md index c7fdeac..2efecd4 100644 --- a/fern/pages/reference/schemas/goto.md +++ b/fern/pages/reference/schemas/goto.md @@ -5,8 +5,8 @@ title: "goTo" ## Referenced In - [Markup definition](/reference/schemas/markup-definition) -- [test](/reference/schemas/test) - [Resolved context](/reference/schemas/resolved-context) +- [test](/reference/schemas/test) ## Fields @@ -18,6 +18,119 @@ goTo | one of:
- string
- object([Go to URL (detailed)](/reference/schem ```json { - "goTo": "example" + "goTo": "https://www.google.com" +} +``` + +```json +{ + "goTo": "/search" +} +``` + +```json +{ + "goTo": { + "url": "https://www.google.com" + } +} +``` + +```json +{ + "goTo": { + "url": "/search", + "origin": "https://www.google.com" + } +} +``` + +```json +{ + "goTo": { + "url": "https://www.example.com", + "waitUntil": { + "networkIdleTime": 500 + } + } +} +``` + +```json +{ + "goTo": { + "url": "https://www.example.com/dashboard", + "waitUntil": { + "find": { + "selector": "[data-testid='dashboard-loaded']" + } + } + } +} +``` + +```json +{ + "goTo": { + "url": "https://www.example.com/app", + "timeout": 60000, + "waitUntil": { + "networkIdleTime": 500, + "domIdleTime": 1000, + "find": { + "selector": ".main-content", + "elementText": "Dashboard" + } + } + } +} +``` + +```json +{ + "goTo": { + "url": "https://www.example.com/app", + "timeout": 60000, + "waitUntil": { + "networkIdleTime": null + } + } +} +``` + +```json +{ + "goTo": { + "url": "https://www.example.com/status", + "waitUntil": { + "find": { + "elementText": "System operational" + } + } + } +} +``` + +```json +{ + "goTo": { + "url": "http://localhost:8092", + "waitUntil": { + "find": { + "selector": "button", + "elementText": "Standard Button", + "elementTestId": "standard-btn", + "elementAria": "Sample Standard Button", + "elementId": "standard-btn", + "elementClass": [ + "btn" + ], + "elementAttribute": { + "type": "button", + "value": "Standard Button" + } + } + } + } } ``` diff --git a/fern/pages/reference/schemas/http-request-detailed.md b/fern/pages/reference/schemas/http-request-detailed.md index 61800fe..40ed70e 100644 --- a/fern/pages/reference/schemas/http-request-detailed.md +++ b/fern/pages/reference/schemas/http-request-detailed.md @@ -11,7 +11,7 @@ title: "HTTP request (detailed)" Field | Type | Description | Default :-- | :-- | :-- | :-- url | string | Optional. URL for the HTTP request.

Pattern: `(^(http://|https://).*|\$[A-Za-z0-9_]+)` | -openApi | one of:
- unknown
- unknown | Optional. No description provided. | +openApi | one of:
- string
- object([openApi](/reference/schemas/openapi)) | Optional. No description provided. | statusCodes | array of integer | Optional. Accepted status codes. If the specified URL returns a code other than what is specified here, the action fails. | ``[200,201]`` method | string | Optional. Method of the HTTP request

Accepted values: `get`, `put`, `post`, `patch`, `delete` | `get` timeout | integer | Optional. Timeout for the HTTP request, in milliseconds. | `60000` @@ -29,6 +29,7 @@ If `aboveVariation`, overwrites the existing output at `path` if the difference ```json { "url": "example", + "openApi": "example", "statusCodes": [ 200, 201 @@ -36,10 +37,7 @@ If `aboveVariation`, overwrites the existing output at `path` if the difference "method": "get", "timeout": 60000, "request": {}, - "response": { - "headers": {}, - "body": {} - }, + "response": {}, "allowAdditionalFields": true, "path": "example", "directory": "example", diff --git a/fern/pages/reference/schemas/httprequest.md b/fern/pages/reference/schemas/httprequest.md index f0f84d5..babae9e 100644 --- a/fern/pages/reference/schemas/httprequest.md +++ b/fern/pages/reference/schemas/httprequest.md @@ -5,8 +5,8 @@ title: "httpRequest" ## Referenced In - [Markup definition](/reference/schemas/markup-definition) -- [test](/reference/schemas/test) - [Resolved context](/reference/schemas/resolved-context) +- [test](/reference/schemas/test) ## Fields @@ -18,6 +18,274 @@ httpRequest | one of:
- string
- object([HTTP request (detailed)](/refer ```json { - "httpRequest": "example" + "httpRequest": "https://reqres.in/api/users" +} +``` + +```json +{ + "httpRequest": { + "url": "https://reqres.in/api/users" + } +} +``` + +```json +{ + "httpRequest": { + "url": "https://reqres.in/api/users/2", + "method": "put", + "request": { + "body": { + "name": "morpheus", + "job": "zion resident" + } + } + } +} +``` + +```json +{ + "httpRequest": { + "url": "https://reqres.in/api/users", + "method": "post", + "request": { + "body": { + "name": "morpheus", + "job": "leader" + } + }, + "response": { + "body": { + "name": "morpheus", + "job": "leader" + } + }, + "statusCodes": [ + 200, + 201 + ] + } +} +``` + +```json +{ + "httpRequest": { + "url": "https://www.api-server.com", + "method": "post", + "timeout": 30000, + "request": { + "body": { + "field": "value" + }, + "headers": { + "header": "value" + }, + "parameters": { + "param": "value" + } + }, + "response": { + "body": { + "field": "value" + }, + "headers": { + "header": "value" + } + }, + "statusCodes": [ + 200 + ] + } +} +``` + +```json +{ + "httpRequest": { + "url": "https://reqres.in/api/users", + "method": "post", + "request": { + "body": { + "name": "morpheus", + "job": "leader" + } + }, + "response": { + "body": { + "name": "morpheus", + "job": "leader" + } + }, + "statusCodes": [ + 200, + 201 + ], + "path": "response.json", + "directory": "media", + "maxVariation": 0.05, + "overwrite": "aboveVariation" + } +} +``` + +```json +{ + "httpRequest": { + "openApi": "getUserById" + } +} +``` + +```json +{ + "httpRequest": { + "openApi": { + "name": "Reqres", + "operationId": "getUserById" + }, + "request": { + "parameters": { + "id": 123 + } + } + } +} +``` + +```json +{ + "httpRequest": { + "openApi": { + "descriptionPath": "https://api.example.com/openapi.json", + "operationId": "getUserById" + }, + "request": { + "parameters": { + "id": 123 + } + } + } +} +``` + +```json +{ + "httpRequest": { + "openApi": { + "descriptionPath": "https://api.example.com/openapi.json", + "operationId": "createUser", + "useExample": "both" + } + } +} +``` + +```json +{ + "httpRequest": { + "openApi": { + "descriptionPath": "https://api.example.com/openapi.json", + "operationId": "updateUser", + "useExample": "request", + "exampleKey": "acme" + } + } +} +``` + +```json +{ + "httpRequest": { + "openApi": { + "descriptionPath": "https://api.example.com/openapi.json", + "operationId": "updateUser", + "useExample": "request", + "exampleKey": "acme", + "headers": { + "Authorization": "Bearer $TOKEN" + } + } + } +} +``` + +```json +{ + "httpRequest": { + "url": "https://www.api-server.com", + "method": "post", + "request": { + "headers": "Content-Type: application/json\\nAuthorization: Bearer token" + } + } +} +``` + +```json +{ + "httpRequest": { + "url": "https://api.example.com/users/123", + "response": { + "required": [ + "id", + "email", + "createdAt" + ] + } + } +} +``` + +```json +{ + "httpRequest": { + "url": "https://api.example.com/users/123", + "response": { + "required": [ + "user.profile.name", + "user.profile.avatar", + "user.settings.notifications" + ] + } + } +} +``` + +```json +{ + "httpRequest": { + "url": "https://api.example.com/orders", + "response": { + "required": [ + "orders[0].id", + "orders[0].total", + "orders[0].items[0].productId" + ] + } + } +} +``` + +```json +{ + "httpRequest": { + "url": "https://api.example.com/users", + "response": { + "required": [ + "sessionToken", + "expiresAt", + "user.id" + ], + "body": { + "status": "success", + "user": { + "role": "admin" + } + } + } + } } ``` diff --git a/fern/pages/reference/schemas/inline-statement-definition.md b/fern/pages/reference/schemas/inline-statement-definition.md index 9898698..d0b3974 100644 --- a/fern/pages/reference/schemas/inline-statement-definition.md +++ b/fern/pages/reference/schemas/inline-statement-definition.md @@ -19,3 +19,13 @@ ignoreEnd | one of:
- string
- array of string | Optional. Regular expre step | one of:
- string
- array of string | Optional. Regular expressions that indicate a step in a test. | ## Examples + +```json +{ + "testStart": "example", + "testEnd": "example", + "ignoreStart": "example", + "ignoreEnd": "example", + "step": "example" +} +``` diff --git a/fern/pages/reference/schemas/integrations-options.md b/fern/pages/reference/schemas/integrations-options.md index 726ff2c..3209f7e 100644 --- a/fern/pages/reference/schemas/integrations-options.md +++ b/fern/pages/reference/schemas/integrations-options.md @@ -12,16 +12,16 @@ Options for connecting to external services. Field | Type | Description | Default :-- | :-- | :-- | :-- -openApi | array of unknown | Optional. No description provided. | +openApi | array of object([openApi](/reference/schemas/openapi)) | Optional. No description provided. | docDetectiveApi | object([Doc Detective Orchestration API](/reference/schemas/doc-detective-orchestration-api)) | Optional. Configuration for Doc Detective Orchestration API integration. | +heretto | array of object(Heretto CMS integration) | Optional. Configuration for Heretto CMS integrations. Each entry specifies a Heretto instance and a scenario to build and test. | ## Examples ```json { "openApi": [], - "docDetectiveApi": { - "apiKey": "example" - } + "docDetectiveApi": {}, + "heretto": [] } ``` diff --git a/fern/pages/reference/schemas/load-cookie-detailed.md b/fern/pages/reference/schemas/load-cookie-detailed.md index c8cf557..6d48182 100644 --- a/fern/pages/reference/schemas/load-cookie-detailed.md +++ b/fern/pages/reference/schemas/load-cookie-detailed.md @@ -11,8 +11,8 @@ title: "Load cookie (detailed)" Field | Type | Description | Default :-- | :-- | :-- | :-- $schema | string | Optional. Optional self-describing schema URI for linters | -name | string | Required. Name of the specific cookie to load.

Pattern: `^[A-Za-z0-9_.-]+$` | -variable | string | Optional. Environment variable name containing the cookie as JSON string.

Pattern: `^[A-Za-z_][A-Za-z0-9_]*$` | +name | string | Required. Name of the specific cookie to load.

Pattern: `[A-Za-z0-9_.-]+` | +variable | string | Optional. Environment variable name containing the cookie as JSON string.

Pattern: `[A-Za-z_][A-Za-z0-9_]*` | path | string | Optional. File path to cookie file, relative to directory. Supports Netscape cookie format. | directory | string | Optional. Directory containing the cookie file. | domain | string | Optional. Specific domain to filter the cookie by when loading from multi-cookie file (optional). | diff --git a/fern/pages/reference/schemas/loadcookie.md b/fern/pages/reference/schemas/loadcookie.md index 6045ed2..121da29 100644 --- a/fern/pages/reference/schemas/loadcookie.md +++ b/fern/pages/reference/schemas/loadcookie.md @@ -5,8 +5,8 @@ title: "loadCookie" ## Referenced In - [Markup definition](/reference/schemas/markup-definition) -- [test](/reference/schemas/test) - [Resolved context](/reference/schemas/resolved-context) +- [test](/reference/schemas/test) ## Fields @@ -18,6 +18,56 @@ loadCookie | one of:
- string
- object([Load cookie (detailed)](/referen ```json { - "loadCookie": "example" + "loadCookie": "session_token" +} +``` + +```json +{ + "loadCookie": "./test-data/auth-session.txt" +} +``` + +```json +{ + "loadCookie": "test_env_cookie" +} +``` + +```json +{ + "loadCookie": { + "name": "auth_cookie", + "variable": "AUTH_COOKIE" + } +} +``` + +```json +{ + "loadCookie": { + "name": "test_cookie", + "path": "test-cookie.txt" + } +} +``` + +```json +{ + "loadCookie": { + "name": "session_token", + "path": "session-token.txt", + "directory": "./test-data" + } +} +``` + +```json +{ + "loadCookie": { + "name": "user_session", + "path": "saved-cookies.txt", + "domain": "app.example.com" + } } ``` diff --git a/fern/pages/reference/schemas/loadvariables.md b/fern/pages/reference/schemas/loadvariables.md index f86644e..e30dc69 100644 --- a/fern/pages/reference/schemas/loadvariables.md +++ b/fern/pages/reference/schemas/loadvariables.md @@ -5,8 +5,8 @@ title: "loadVariables" ## Referenced In - [Markup definition](/reference/schemas/markup-definition) -- [test](/reference/schemas/test) - [Resolved context](/reference/schemas/resolved-context) +- [test](/reference/schemas/test) ## Fields @@ -18,6 +18,6 @@ loadVariables | string | Required. Load environment variables from the specified ```json { - "loadVariables": "example" + "loadVariables": ".env" } ``` diff --git a/fern/pages/reference/schemas/markup-definition.md b/fern/pages/reference/schemas/markup-definition.md index 3c87e02..23cc5c1 100644 --- a/fern/pages/reference/schemas/markup-definition.md +++ b/fern/pages/reference/schemas/markup-definition.md @@ -13,13 +13,14 @@ Field | Type | Description | Default name | string | Optional. Name of the markup definition | regex | one of:
- string
- array of string | Optional. Regular expressions to match the markup type. | batchMatches | boolean | Optional. If `true`, all matches are combined into a single string. | `false` -actions | one of:
- string
- array of one of: string, object(step) | Optional. Actions to perform when the markup type is detected. | +actions | one of:
- string
- array of one of:
- string
- object([Common](/reference/schemas/common)) | Optional. Actions to perform when the markup type is detected. | ## Examples ```json { "name": "example", + "regex": "example", "batchMatches": false, "actions": "checkLink" } diff --git a/fern/pages/reference/schemas/openapi.md b/fern/pages/reference/schemas/openapi.md index b7b9fc9..762f996 100644 --- a/fern/pages/reference/schemas/openapi.md +++ b/fern/pages/reference/schemas/openapi.md @@ -8,9 +8,9 @@ OpenAPI description and configuration. - [HTTP request (detailed)](/reference/schemas/http-request-detailed) - [Integrations options](/reference/schemas/integrations-options) +- [Resolved context](/reference/schemas/resolved-context) - [specification](/reference/schemas/specification) - [test](/reference/schemas/test) -- [Resolved context](/reference/schemas/resolved-context) ## Fields diff --git a/fern/pages/reference/schemas/record.md b/fern/pages/reference/schemas/record.md index 637f19e..7fa1c26 100644 --- a/fern/pages/reference/schemas/record.md +++ b/fern/pages/reference/schemas/record.md @@ -5,8 +5,8 @@ title: "record" ## Referenced In - [Markup definition](/reference/schemas/markup-definition) -- [test](/reference/schemas/test) - [Resolved context](/reference/schemas/resolved-context) +- [test](/reference/schemas/test) ## Fields @@ -18,6 +18,22 @@ record | one of:
- string
- object([Record (detailed)](/reference/schema ```json { - "record": "example" + "record": true +} +``` + +```json +{ + "record": "results.mp4" +} +``` + +```json +{ + "record": { + "path": "results.mp4", + "directory": "static/media", + "overwrite": "true" + } } ``` diff --git a/fern/pages/reference/schemas/request-body-object.md b/fern/pages/reference/schemas/request-body-object.md index 917f663..c596ff1 100644 --- a/fern/pages/reference/schemas/request-body-object.md +++ b/fern/pages/reference/schemas/request-body-object.md @@ -14,3 +14,7 @@ Field | Type | Description | Default :-- | :-- | :-- | :-- ## Examples + +```json +{} +``` diff --git a/fern/pages/reference/schemas/request-headers-object.md b/fern/pages/reference/schemas/request-headers-object.md index 5a09d03..a244a50 100644 --- a/fern/pages/reference/schemas/request-headers-object.md +++ b/fern/pages/reference/schemas/request-headers-object.md @@ -14,3 +14,7 @@ Field | Type | Description | Default :-- | :-- | :-- | :-- ## Examples + +```json +{} +``` diff --git a/fern/pages/reference/schemas/request-parameters.md b/fern/pages/reference/schemas/request-parameters.md index 04389f1..7f87627 100644 --- a/fern/pages/reference/schemas/request-parameters.md +++ b/fern/pages/reference/schemas/request-parameters.md @@ -14,3 +14,7 @@ Field | Type | Description | Default :-- | :-- | :-- | :-- ## Examples + +```json +{} +``` diff --git a/fern/pages/reference/schemas/resolved-context.md b/fern/pages/reference/schemas/resolved-context.md index 576d6bd..5302aa1 100644 --- a/fern/pages/reference/schemas/resolved-context.md +++ b/fern/pages/reference/schemas/resolved-context.md @@ -12,7 +12,7 @@ Field | Type | Description | Default :-- | :-- | :-- | :-- platform | string | Optional. Platform to run the test on. This is a resolved version of the `platforms` property. | browser | object([Browser](/reference/schemas/browser)) | Optional. Browser configuration. | -openApi | array of unknown | Optional. No description provided. | +openApi | array of object([openApi](/reference/schemas/openapi)) | Optional. No description provided. | steps | array of object(step) | Optional. Steps to perform as part of the test. Performed in the sequence defined. If one or more actions fail, the test fails. By default, if a step fails, the test stops and the remaining steps are not executed. | ## Examples diff --git a/fern/pages/reference/schemas/response-body-object.md b/fern/pages/reference/schemas/response-body-object.md index 562fde9..6f59094 100644 --- a/fern/pages/reference/schemas/response-body-object.md +++ b/fern/pages/reference/schemas/response-body-object.md @@ -14,3 +14,7 @@ Field | Type | Description | Default :-- | :-- | :-- | :-- ## Examples + +```json +{} +``` diff --git a/fern/pages/reference/schemas/response-headers.md b/fern/pages/reference/schemas/response-headers.md index cec2594..0c2be98 100644 --- a/fern/pages/reference/schemas/response-headers.md +++ b/fern/pages/reference/schemas/response-headers.md @@ -14,3 +14,7 @@ Field | Type | Description | Default :-- | :-- | :-- | :-- ## Examples + +```json +{} +``` diff --git a/fern/pages/reference/schemas/response.md b/fern/pages/reference/schemas/response.md index b9bfbb9..e4452d5 100644 --- a/fern/pages/reference/schemas/response.md +++ b/fern/pages/reference/schemas/response.md @@ -12,12 +12,14 @@ Field | Type | Description | Default :-- | :-- | :-- | :-- headers | object([Response headers](/reference/schemas/response-headers)) | Optional. Headers expected in the response, in key/value format. If one or more `responseHeaders` entries aren't present in the response, the step fails. | ``{}`` body | one of:
- object([Response body object](/reference/schemas/response-body-object))
- array of unknown
- string | Optional. JSON object expected in the response. If one or more key/value pairs aren't present in the response, the step fails. | ``{}`` +required | array of string | Optional. Array of field paths that must exist in the response body. Uses dot notation for nested fields (e.g., 'user.name') and bracket notation for array indices (e.g., 'items[0].id'). Fields must be present but may have any value including null. | ``[]`` ## Examples ```json { "headers": {}, - "body": {} + "body": {}, + "required": [] } ``` diff --git a/fern/pages/reference/schemas/run-code-detailed.md b/fern/pages/reference/schemas/run-code-detailed.md index df64ed0..5e16d34 100644 --- a/fern/pages/reference/schemas/run-code-detailed.md +++ b/fern/pages/reference/schemas/run-code-detailed.md @@ -18,7 +18,7 @@ exitCodes | array of integer | Optional. Expected exit codes of the command. If stdio | string | Optional. Content expected in the command's output. If the expected content can't be found in the command's output (either stdout or stderr), the step fails. Supports strings and regular expressions. To use a regular expression, the string must start and end with a forward slash, like in `/^hello-world.*/`. | path | string | Optional. File path to save the command's output, relative to `directory`. | directory | string | Optional. Directory to save the command's output. If the directory doesn't exist, creates the directory. If not specified, the directory is your media directory. | -maxVariation | number | Optional. Allowed variation in percentage of text different between the current output and previously saved output. If the difference between the current output and the previous output is greater than `maxVariation`, the step fails. If output doesn't exist at `path`, this value is ignored.

Minimum: 0. Maximum: 1 | `0` +maxVariation | number | Optional. Allowed variation as a fraction (0 to 1) of text different between the current output and previously saved output. For example, 0.1 means 10%. If the difference between the current output and the previous output is greater than `maxVariation`, the step fails. If output doesn't exist at `path`, this value is ignored.

Minimum: 0. Maximum: 1

**Migration note:** Values are fractions, not percentages. If you previously used a percentage-style value like `10` (for 10%), update it to `0.1`. | `0` overwrite | string | Optional. If `true`, overwrites the existing output at `path` if it exists. If `aboveVariation`, overwrites the existing output at `path` if the difference between the new output and the existing output is greater than `maxVariation`.

Accepted values: `true`, `false`, `aboveVariation` | `aboveVariation` timeout | integer | Optional. Max time in milliseconds the command is allowed to run. If the command runs longer than this, the step fails. | `60000` diff --git a/fern/pages/reference/schemas/run-shell-command-detailed.md b/fern/pages/reference/schemas/run-shell-command-detailed.md index 96b9d97..9390ca2 100644 --- a/fern/pages/reference/schemas/run-shell-command-detailed.md +++ b/fern/pages/reference/schemas/run-shell-command-detailed.md @@ -4,8 +4,8 @@ title: "Run shell command (detailed)" ## Referenced In -- [runShell](/reference/schemas/runshell) - [File type (executable)](/reference/schemas/file-type-executable) +- [runShell](/reference/schemas/runshell) ## Fields @@ -18,7 +18,7 @@ exitCodes | array of integer | Optional. Expected exit codes of the command. If stdio | string | Optional. Content expected in the command's stdout or stderr. If the expected content can't be found in the command's stdout or stderr, the step fails. Supports strings and regular expressions. To use a regular expression, the string must start and end with a forward slash, like in `/^hello-world.*/`. | path | string | Optional. File path to save the command's output, relative to `directory`. | directory | string | Optional. Directory to save the command's output. If the directory doesn't exist, creates the directory. If not specified, the directory is your media directory. | -maxVariation | number | Optional. Allowed variation in percentage of text different between the current output and previously saved output. If the difference between the current output and the previous output is greater than `maxVariation`, the step fails. If output doesn't exist at `path`, this value is ignored.

Minimum: 0. Maximum: 1 | `0` +maxVariation | number | Optional. Allowed variation as a fraction (0 to 1) of text different between the current output and previously saved output. For example, 0.1 means 10%. If the difference between the current output and the previous output is greater than `maxVariation`, the step fails. If output doesn't exist at `path`, this value is ignored.

Minimum: 0. Maximum: 1

**Migration note:** Values are fractions, not percentages. If you previously used a percentage-style value like `10` (for 10%), update it to `0.1`. | `0` overwrite | string | Optional. If `true`, overwrites the existing output at `path` if it exists. If `aboveVariation`, overwrites the existing output at `path` if the difference between the new output and the existing output is greater than `maxVariation`.

Accepted values: `true`, `false`, `aboveVariation` | `aboveVariation` timeout | integer | Optional. Max time in milliseconds the command is allowed to run. If the command runs longer than this, the step fails. | `60000` diff --git a/fern/pages/reference/schemas/runcode.md b/fern/pages/reference/schemas/runcode.md index 403cad0..ae4fa50 100644 --- a/fern/pages/reference/schemas/runcode.md +++ b/fern/pages/reference/schemas/runcode.md @@ -5,8 +5,8 @@ title: "runCode" ## Referenced In - [Markup definition](/reference/schemas/markup-definition) -- [test](/reference/schemas/test) - [Resolved context](/reference/schemas/resolved-context) +- [test](/reference/schemas/test) ## Fields @@ -15,3 +15,56 @@ Field | Type | Description | Default runCode | object([Run code (detailed)](/reference/schemas/run-code-detailed)) | Required. Assemble and run code. | ## Examples + +```json +{ + "runCode": { + "language": "javascript", + "code": "console.log(`Hello, ${process.env.USER}!`);" + } +} +``` + +```json +{ + "runCode": { + "language": "bash", + "code": "docker run hello-world", + "timeout": 20000, + "exitCodes": [ + 0 + ], + "stdio": "Hello from Docker!" + } +} +``` + +```json +{ + "runCode": { + "language": "javascript", + "code": "process.exit(1)", + "exitCodes": [ + 1 + ] + } +} +``` + +```json +{ + "runCode": { + "language": "python", + "code": "print('Hello from Python!')", + "workingDirectory": ".", + "exitCodes": [ + 0 + ], + "stdio": "Hello from Python!", + "path": "python-output.txt", + "directory": "output", + "maxVariation": 0.1, + "overwrite": "aboveVariation" + } +} +``` diff --git a/fern/pages/reference/schemas/runshell.md b/fern/pages/reference/schemas/runshell.md index e946eae..3d4180e 100644 --- a/fern/pages/reference/schemas/runshell.md +++ b/fern/pages/reference/schemas/runshell.md @@ -4,9 +4,10 @@ title: "runShell" ## Referenced In +- [File type (executable)](/reference/schemas/file-type-executable) - [Markup definition](/reference/schemas/markup-definition) -- [test](/reference/schemas/test) - [Resolved context](/reference/schemas/resolved-context) +- [test](/reference/schemas/test) ## Fields @@ -18,6 +19,84 @@ runShell | one of:
- string
- object([Run shell command (detailed)](/ref ```json { - "runShell": "example" + "runShell": "docker run hello-world" +} +``` + +```json +{ + "runShell": { + "command": "echo", + "args": [ + "$USER" + ] + } +} +``` + +```json +{ + "runShell": { + "command": "echo", + "args": [ + "hello-world" + ] + } +} +``` + +```json +{ + "runShell": { + "command": "docker run hello-world", + "timeout": 20000, + "exitCodes": [ + 0 + ], + "stdio": "Hello from Docker!" + } +} +``` + +```json +{ + "runShell": { + "command": "false", + "exitCodes": [ + 1 + ] + } +} +``` + +```json +{ + "runShell": { + "command": "echo", + "args": [ + "setup" + ], + "exitCodes": [ + 0 + ], + "stdio": "/.*?/" + } +} +``` + +```json +{ + "runShell": { + "command": "docker run hello-world", + "workingDirectory": ".", + "exitCodes": [ + 0 + ], + "stdio": "Hello from Docker!", + "path": "docker-output.txt", + "directory": "output", + "maxVariation": 0.1, + "overwrite": "aboveVariation" + } } ``` diff --git a/fern/pages/reference/schemas/save-cookie-detailed.md b/fern/pages/reference/schemas/save-cookie-detailed.md index db5fcbf..21b77ce 100644 --- a/fern/pages/reference/schemas/save-cookie-detailed.md +++ b/fern/pages/reference/schemas/save-cookie-detailed.md @@ -11,8 +11,8 @@ title: "Save cookie (detailed)" Field | Type | Description | Default :-- | :-- | :-- | :-- $schema | string | Optional. Optional self-describing schema URI for linters | -name | string | Required. Name of the specific cookie to save.

Pattern: `^[A-Za-z0-9_.-]+$` | -variable | string | Optional. Environment variable name to store the cookie as JSON string.

Pattern: `^[A-Za-z_][A-Za-z0-9_]*$` | +name | string | Required. Name of the specific cookie to save.

Pattern: `[A-Za-z0-9_.-]+` | +variable | string | Optional. Environment variable name to store the cookie as JSON string.

Pattern: `[A-Za-z_][A-Za-z0-9_]*` | path | string | Optional. File path to save the cookie, relative to directory. Uses Netscape cookie format. | directory | string | Optional. Directory to save the cookie file. If not specified, uses output directory. | overwrite | boolean | Optional. Whether to overwrite existing cookie file. | `false` diff --git a/fern/pages/reference/schemas/savecookie.md b/fern/pages/reference/schemas/savecookie.md index f2f6312..79b808b 100644 --- a/fern/pages/reference/schemas/savecookie.md +++ b/fern/pages/reference/schemas/savecookie.md @@ -5,8 +5,8 @@ title: "saveCookie" ## Referenced In - [Markup definition](/reference/schemas/markup-definition) -- [test](/reference/schemas/test) - [Resolved context](/reference/schemas/resolved-context) +- [test](/reference/schemas/test) ## Fields @@ -18,6 +18,61 @@ saveCookie | one of:
- string
- object([Save cookie (detailed)](/referen ```json { - "saveCookie": "example" + "saveCookie": "session_token" +} +``` + +```json +{ + "saveCookie": "test_env_cookie" +} +``` + +```json +{ + "saveCookie": { + "name": "auth_cookie", + "path": "auth-cookie.txt" + } +} +``` + +```json +{ + "saveCookie": { + "name": "session_token", + "variable": "SESSION_TOKEN" + } +} +``` + +```json +{ + "saveCookie": { + "name": "test_cookie", + "path": "test-cookie.txt", + "overwrite": true + } +} +``` + +```json +{ + "saveCookie": { + "name": "user_session", + "path": "user-session.txt", + "directory": "./test-data", + "overwrite": true + } +} +``` + +```json +{ + "saveCookie": { + "name": "login_token", + "path": "login-token.txt", + "domain": "app.example.com" + } } ``` diff --git a/fern/pages/reference/schemas/screenshot.md b/fern/pages/reference/schemas/screenshot.md index 0b021cf..7499b6c 100644 --- a/fern/pages/reference/schemas/screenshot.md +++ b/fern/pages/reference/schemas/screenshot.md @@ -5,8 +5,8 @@ title: "screenshot" ## Referenced In - [Markup definition](/reference/schemas/markup-definition) -- [test](/reference/schemas/test) - [Resolved context](/reference/schemas/resolved-context) +- [test](/reference/schemas/test) ## Fields @@ -18,6 +18,68 @@ screenshot | one of:
- string
- object([Capture screenshot (detailed)](/ ```json { - "screenshot": "example" + "screenshot": true +} +``` + +```json +{ + "screenshot": "image.png" +} +``` + +```json +{ + "screenshot": "static/images/image.png" +} +``` + +```json +{ + "screenshot": "/User/manny/projects/doc-detective/static/images/image.png" +} +``` + +```json +{ + "screenshot": { + "path": "image.png", + "directory": "static/images", + "maxVariation": 0.1, + "overwrite": "aboveVariation", + "crop": "#elementToScreenshot" + } +} +``` + +```json +{ + "screenshot": { + "path": "image.png", + "directory": "static/images", + "maxVariation": 0.1, + "overwrite": "aboveVariation" + } +} +``` + +```json +{ + "screenshot": { + "path": "image.png", + "directory": "static/images", + "maxVariation": 0.1, + "overwrite": "aboveVariation", + "crop": { + "selector": "#elementToScreenshot", + "elementText": "Element text", + "padding": { + "top": 0, + "right": 0, + "bottom": 0, + "left": 0 + } + } + } } ``` diff --git a/fern/pages/reference/schemas/specification.md b/fern/pages/reference/schemas/specification.md index d52d70d..3fca20d 100644 --- a/fern/pages/reference/schemas/specification.md +++ b/fern/pages/reference/schemas/specification.md @@ -12,7 +12,7 @@ description | string | Optional. Description of the test specification. | specPath | string | Optional. Path to the test specification. | contentPath | string | Optional. Path to the content that the specification is associated with. | runOn | array of object([context](/reference/schemas/context)) | Optional. Contexts to run the test in. Overrides contexts defined at the config and spec levels. | -openApi | array of unknown | Optional. No description provided. | +openApi | array of object([openApi](/reference/schemas/openapi)) | Optional. No description provided. | tests | array of object([test](/reference/schemas/test)) | Required. [Tests](test) to perform. | ## Examples diff --git a/fern/pages/reference/schemas/stoprecord.md b/fern/pages/reference/schemas/stoprecord.md index 865e092..f0eb404 100644 --- a/fern/pages/reference/schemas/stoprecord.md +++ b/fern/pages/reference/schemas/stoprecord.md @@ -5,8 +5,8 @@ title: "stopRecord" ## Referenced In - [Markup definition](/reference/schemas/markup-definition) -- [test](/reference/schemas/test) - [Resolved context](/reference/schemas/resolved-context) +- [test](/reference/schemas/test) ## Fields diff --git a/fern/pages/reference/schemas/test.md b/fern/pages/reference/schemas/test.md index bb898a4..89f9ff8 100644 --- a/fern/pages/reference/schemas/test.md +++ b/fern/pages/reference/schemas/test.md @@ -17,7 +17,7 @@ description | string | Optional. Description of the test. | contentPath | string | Optional. Path to the content that the test is associated with. | detectSteps | boolean | Optional. Whether or not to detect steps in input files based on markup regex. | `true` runOn | array of object([context](/reference/schemas/context)) | Optional. Contexts to run the test in. Overrides contexts defined at the config and spec levels. | -openApi | array of unknown | Optional. No description provided. | +openApi | array of object([openApi](/reference/schemas/openapi)) | Optional. No description provided. | before | string | Optional. Path to a test specification to perform before this test, while maintaining this test's context. Useful for setting up testing environments. Only the `steps` property is used from the first test in the setup spec. | after | string | Optional. Path to a test specification to perform after this test, while maintaining this test's context. Useful for cleaning up testing environments. Only the `steps` property is used from the first test in the cleanup spec. | steps | array of object(step) | Optional. Steps to perform as part of the test. Performed in the sequence defined. If one or more actions fail, the test fails. By default, if a step fails, the test stops and the remaining steps are not executed. | diff --git a/fern/pages/reference/schemas/type-keys-detailed.md b/fern/pages/reference/schemas/type-keys-detailed.md index 3b90dfd..69c3ae2 100644 --- a/fern/pages/reference/schemas/type-keys-detailed.md +++ b/fern/pages/reference/schemas/type-keys-detailed.md @@ -14,6 +14,12 @@ Field | Type | Description | Default keys | one of:
- string
- array of string | Required. Sequence of keys to enter. | inputDelay | number | Optional. Delay in milliseconds between each key press during a recording | `100` selector | string | Optional. Selector for the element to type into. If not specified, the typing occurs in the active element. | +elementText | string | Optional. Display text of the element to type into. If combined with other element finding fields, the element must match all specified criteria. | +elementId | string | Optional. ID attribute of the element to find. Supports exact match or regex pattern using /pattern/ syntax. | +elementTestId | string | Optional. data-testid attribute of the element to find. Supports exact match or regex pattern using /pattern/ syntax. | +elementClass | one of:
- string
- array of string | Optional. Class or array of classes that the element must have. Each class supports exact match or regex pattern using /pattern/ syntax. Element must have all specified classes. | +elementAttribute | object | Optional. Object of attribute key-value pairs that the element must have. Values can be strings (supporting /pattern/ regex), numbers, or booleans. Boolean true matches attribute presence, false matches absence. | +elementAria | string | Optional. Computed accessible name of the element per ARIA specification. Supports exact match or regex pattern using /pattern/ syntax. | ## Examples @@ -21,6 +27,12 @@ selector | string | Optional. Selector for the element to type into. If not spec { "keys": "example", "inputDelay": 100, - "selector": "example" + "selector": "example", + "elementText": "example", + "elementId": "example", + "elementTestId": "example", + "elementClass": "example", + "elementAttribute": {}, + "elementAria": "example" } ``` diff --git a/fern/pages/reference/schemas/type.md b/fern/pages/reference/schemas/type.md index 5f03b05..f6f04e5 100644 --- a/fern/pages/reference/schemas/type.md +++ b/fern/pages/reference/schemas/type.md @@ -4,9 +4,10 @@ title: "type" ## Referenced In +- [Find element (detailed)](/reference/schemas/find-element-detailed) - [Markup definition](/reference/schemas/markup-definition) -- [test](/reference/schemas/test) - [Resolved context](/reference/schemas/resolved-context) +- [test](/reference/schemas/test) ## Fields @@ -15,3 +16,56 @@ Field | Type | Description | Default type | one of:
- one of:
- string
- array of string
- object([Type keys (detailed)](/reference/schemas/type-keys-detailed)) | Required. Type keys. To type special keys, begin and end the string with `$` and use the special key's keyword. For example, to type the Escape key, enter `$ESCAPE$`. | ## Examples + +```json +{ + "type": "kittens" +} +``` + +```json +{ + "type": [ + "$ENTER$" + ] +} +``` + +```json +{ + "type": [ + "kittens", + "$ENTER$" + ] +} +``` + +```json +{ + "type": { + "keys": "kittens" + } +} +``` + +```json +{ + "type": { + "keys": [ + "$ENTER$" + ] + } +} +``` + +```json +{ + "type": { + "keys": [ + "kittens", + "$ENTER$" + ], + "inputDelay": 500 + } +} +``` diff --git a/fern/pages/reference/schemas/wait.md b/fern/pages/reference/schemas/wait.md index adf5b2c..9afd6c5 100644 --- a/fern/pages/reference/schemas/wait.md +++ b/fern/pages/reference/schemas/wait.md @@ -5,8 +5,8 @@ title: "wait" ## Referenced In - [Markup definition](/reference/schemas/markup-definition) -- [test](/reference/schemas/test) - [Resolved context](/reference/schemas/resolved-context) +- [test](/reference/schemas/test) ## Fields @@ -21,3 +21,15 @@ wait | one of:
- number
- string
- boolean | Required. Pause (in mil "wait": 5000 } ``` + +```json +{ + "wait": "$WAIT_DURATION" +} +``` + +```json +{ + "wait": true +} +``` diff --git a/package-lock.json b/package-lock.json index 65fe773..01f1824 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,10 @@ "dependencies": { "fern-api": "^3.91.5" }, + "devDependencies": { + "tsx": "^4.19.0", + "typescript": "^5.7.0" + }, "engines": { "node": "^22.12.0 || ^24.11.1", "npm": "^10.9.2 || ^11.6.2", @@ -153,6 +157,448 @@ "node": ">= 10" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -187,6 +633,48 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, "node_modules/fern-api": { "version": "3.91.5", "resolved": "https://registry.npmjs.org/fern-api/-/fern-api-3.91.5.tgz", @@ -199,6 +687,34 @@ "fern": "cli.cjs" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -208,6 +724,16 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -233,6 +759,40 @@ "engines": { "node": ">=8" } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } } } } diff --git a/package.json b/package.json index 3d16aa9..94dcff4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "setup": "npm i -g mdx2vast fern-api doc-detective@latest", "dev": "fern docs dev", - "test": "npx doc-detective" + "test": "npx doc-detective", + "generate": "npx tsx scripts/generate-references.ts" }, "engines": { "node": "^22.12.0 || ^24.11.1", @@ -15,5 +16,9 @@ }, "dependencies": { "fern-api": "^3.91.5" + }, + "devDependencies": { + "typescript": "^5.7.0", + "tsx": "^4.19.0" } } diff --git a/scripts/generate-references.ts b/scripts/generate-references.ts new file mode 100644 index 0000000..f5df931 --- /dev/null +++ b/scripts/generate-references.ts @@ -0,0 +1,1397 @@ +/** + * generate-references.ts + * + * Reads JSON schemas and TypeScript source from a local clone of + * doc-detective/doc-detective and generates: + * 1. Schema .md pages in fern/pages/reference/schemas/ + * 2. API function .mdx stubs in fern/pages/reference/api/ + * + * Usage: npx tsx scripts/generate-references.ts + */ + +import * as fs from "node:fs"; +import * as path from "node:path"; +import * as ts from "typescript"; +import { fileURLToPath } from "node:url"; + +// --------------------------------------------------------------------------- +// Paths +// --------------------------------------------------------------------------- +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const ROOT = path.resolve(__dirname, ".."); +const SOURCE_REPO = path.resolve(ROOT, "..", "doc-detective"); +if (!fs.existsSync(SOURCE_REPO)) { + console.error( + `Error: Source repository not found at ${SOURCE_REPO}\n` + + `Clone the doc-detective/doc-detective repository next to this repo:\n` + + ` git clone https://github.com/doc-detective/doc-detective.git ${SOURCE_REPO}` + ); + process.exit(1); +} +const SCHEMAS_JSON = path.join( + SOURCE_REPO, + "src/common/src/schemas/schemas.json" +); +const SCHEMA_OUT_DIR = path.join(ROOT, "fern/pages/reference/schemas"); +const API_OUT_DIR = path.join(ROOT, "fern/pages/reference/api"); + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface SchemaLike { + $schema?: string; + title?: string; + description?: string; + type?: string | string[]; + anyOf?: SchemaLike[]; + oneOf?: SchemaLike[]; + allOf?: SchemaLike[]; + properties?: Record; + items?: SchemaLike; + required?: string[]; + enum?: (string | number | boolean)[]; + default?: unknown; + pattern?: string; + readOnly?: boolean; + minimum?: number; + maximum?: number; + additionalProperties?: boolean | SchemaLike; + components?: { schemas?: Record }; + examples?: unknown[]; + $comment?: string; + transform?: string[]; + minItems?: number; +} + +interface PageDef { + /** Output filename, e.g. "checklink.md" */ + file: string; + /** Display title for frontmatter */ + title: string; + /** Optional description paragraph */ + description?: string; + /** Schema object to render fields from */ + schema: SchemaLike; + /** + * "direct" – render properties directly + * "wrapper" – step-level wrapper: one field named `wrapField` with type from schema + * "empty" – object with additionalProperties, no known properties + */ + mode: "direct" | "wrapper" | "empty"; + /** For wrapper mode: the field name */ + wrapField?: string; +} + +// --------------------------------------------------------------------------- +// Load schemas +// --------------------------------------------------------------------------- +const allSchemas: Record = JSON.parse( + fs.readFileSync(SCHEMAS_JSON, "utf8") +); + +// --------------------------------------------------------------------------- +// Helpers: title → slug → filename +// --------------------------------------------------------------------------- +function titleToSlug(title: string): string { + return title + .toLowerCase() + .replace(/[()]/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, ""); +} + +/** Map a (title, filename) so we can resolve cross-references */ +const titleToFileMap = new Map(); +const fileToTitleMap = new Map(); + +function registerPage(title: string, file: string) { + titleToFileMap.set(title, file); + fileToTitleMap.set(file, title); +} + +// --------------------------------------------------------------------------- +// Build all page definitions +// --------------------------------------------------------------------------- + +function buildPageDefs(): PageDef[] { + const defs: PageDef[] = []; + + // Helper to get schema or throw + const get = (key: string): SchemaLike => { + const s = allSchemas[key]; + if (!s) throw new Error(`Schema not found: ${key}`); + return s; + }; + + const comp = (key: string, compName: string): SchemaLike => { + const s = get(key); + const c = s.components?.schemas?.[compName]; + if (!c) throw new Error(`Component ${compName} not found in ${key}`); + return c; + }; + + // ── Action step wrapper pages ────────────────────────────────────────── + const actions: Array<{ + key: string; + file: string; + wrapField: string; + }> = [ + { key: "checkLink_v3", file: "checklink.md", wrapField: "checkLink" }, + { key: "click_v3", file: "click.md", wrapField: "click" }, + { + key: "dragAndDrop_v3", + file: "draganddrop.md", + wrapField: "dragAndDrop", + }, + { key: "find_v3", file: "find.md", wrapField: "find" }, + { key: "goTo_v3", file: "goto.md", wrapField: "goTo" }, + { key: "httpRequest_v3", file: "httprequest.md", wrapField: "httpRequest" }, + { key: "loadCookie_v3", file: "loadcookie.md", wrapField: "loadCookie" }, + { + key: "loadVariables_v3", + file: "loadvariables.md", + wrapField: "loadVariables", + }, + { key: "record_v3", file: "record.md", wrapField: "record" }, + { key: "runCode_v3", file: "runcode.md", wrapField: "runCode" }, + { key: "runShell_v3", file: "runshell.md", wrapField: "runShell" }, + { key: "saveCookie_v3", file: "savecookie.md", wrapField: "saveCookie" }, + { key: "screenshot_v3", file: "screenshot.md", wrapField: "screenshot" }, + { key: "stopRecord_v3", file: "stoprecord.md", wrapField: "stopRecord" }, + { key: "type_v3", file: "type.md", wrapField: "type" }, + { key: "wait_v3", file: "wait.md", wrapField: "wait" }, + ]; + + for (const a of actions) { + const schema = get(a.key); + defs.push({ + file: a.file, + title: schema.title || a.wrapField, + description: undefined, // wrapper pages don't show description + schema, + mode: "wrapper", + wrapField: a.wrapField, + }); + } + + // ── Action detailed/object pages ─────────────────────────────────────── + const detailedPages: Array<{ + key: string; + compName: string; + file: string; + }> = [ + { + key: "checkLink_v3", + compName: "object", + file: "check-link-detailed.md", + }, + { + key: "click_v3", + compName: "object", + file: "click-element-detailed.md", + }, + { + key: "find_v3", + compName: "object", + file: "find-element-detailed.md", + }, + { key: "goTo_v3", compName: "object", file: "go-to-url-detailed.md" }, + { + key: "httpRequest_v3", + compName: "object", + file: "http-request-detailed.md", + }, + { + key: "loadCookie_v3", + compName: "object", + file: "load-cookie-detailed.md", + }, + { key: "record_v3", compName: "object", file: "record-detailed.md" }, + { key: "runCode_v3", compName: "object", file: "run-code-detailed.md" }, + { + key: "runShell_v3", + compName: "object", + file: "run-shell-command-detailed.md", + }, + { + key: "saveCookie_v3", + compName: "object", + file: "save-cookie-detailed.md", + }, + { + key: "screenshot_v3", + compName: "object", + file: "capture-screenshot-detailed.md", + }, + { key: "type_v3", compName: "object", file: "type-keys-detailed.md" }, + ]; + + for (const d of detailedPages) { + const schema = comp(d.key, d.compName); + defs.push({ + file: d.file, + title: schema.title || d.file.replace(".md", ""), + description: schema.description, + schema, + mode: "direct", + }); + } + + // ── Direct schemas ───────────────────────────────────────────────────── + const directSchemas: Array<{ key: string; file: string }> = [ + { key: "config_v3", file: "config.md" }, + { key: "context_v3", file: "context.md" }, + { key: "test_v3", file: "test.md" }, + { key: "spec_v3", file: "specification.md" }, + { key: "openApi_v3", file: "openapi.md" }, + { key: "dragAndDrop_v3", file: "draganddrop.md" }, + ]; + + for (const d of directSchemas) { + // dragAndDrop is already registered as a wrapper above, skip re-adding + if (d.file === "draganddrop.md") continue; + const schema = get(d.key); + defs.push({ + file: d.file, + title: schema.title || d.key, + description: schema.description, + schema, + mode: "direct", + }); + } + + // ── step_v3 common fields ────────────────────────────────────────────── + { + const schema = comp("step_v3", "common"); + defs.push({ + file: "common.md", + title: schema.title || "Common", + description: schema.description, + schema, + mode: "direct", + }); + } + + // ── context_v3 sub-schemas ───────────────────────────────────────────── + { + const browser = comp("context_v3", "browser"); + defs.push({ + file: "browser.md", + title: browser.title || "Browser", + description: browser.description, + schema: browser, + mode: "direct", + }); + + // Browser sub-schemas: window and viewport + const window = browser.properties?.window; + if (window) { + defs.push({ + file: "browser-window.md", + title: window.title || "Browser Window", + description: window.description, + schema: window, + mode: "direct", + }); + } + const viewport = browser.properties?.viewport; + if (viewport) { + defs.push({ + file: "browser-viewport.md", + title: viewport.title || "Browser Viewport", + description: viewport.description, + schema: viewport, + mode: "direct", + }); + } + } + + // ── config_v3 sub-schemas ────────────────────────────────────────────── + { + const cfg = get("config_v3"); + + // environment + const env = comp("config_v3", "environment"); + defs.push({ + file: "environment-details.md", + title: env.title || "Environment details", + description: env.description, + schema: env, + mode: "direct", + }); + + // markupDefinition + const markup = comp("config_v3", "markupDefinition"); + defs.push({ + file: "markup-definition.md", + title: markup.title || "Markup definition", + description: markup.description, + schema: markup, + mode: "direct", + }); + + // inlineStatements + const inline = comp("config_v3", "inlineStatements"); + defs.push({ + file: "inline-statement-definition.md", + title: inline.title || "Inline statement definition", + description: inline.description, + schema: inline, + mode: "direct", + }); + + // integrations + const integ = cfg.properties?.integrations; + if (integ) { + defs.push({ + file: "integrations-options.md", + title: "Integrations options", + description: integ.description, + schema: integ, + mode: "direct", + }); + + // docDetectiveApi + const ddApi = integ.properties?.docDetectiveApi; + if (ddApi) { + defs.push({ + file: "doc-detective-orchestration-api.md", + title: ddApi.title || "Doc Detective Orchestration API", + description: ddApi.description, + schema: ddApi, + mode: "direct", + }); + } + } + + // telemetry + const telem = cfg.properties?.telemetry; + if (telem) { + defs.push({ + file: "telemetry-options.md", + title: "Telemetry options", + description: telem.description, + schema: telem, + mode: "direct", + }); + } + + // fileTypes sub-schemas: custom, executable + const ft = cfg.properties?.fileTypes; + if (ft?.anyOf?.[0]?.items?.anyOf) { + const ftItems = ft.anyOf[0].items.anyOf; + const custom = ftItems.find( + (i: SchemaLike) => i.title === "File type (custom)" + ); + if (custom) { + defs.push({ + file: "file-type-custom.md", + title: custom.title!, + description: custom.description, + schema: custom, + mode: "direct", + }); + } + const exec = ftItems.find( + (i: SchemaLike) => i.title === "File type (executable)" + ); + if (exec) { + defs.push({ + file: "file-type-executable.md", + title: exec.title!, + description: exec.description, + schema: exec, + mode: "direct", + }); + } + } + } + + // ── httpRequest sub-schemas ──────────────────────────────────────────── + { + const hrObj = comp("httpRequest_v3", "object"); + + // request + const request = hrObj.properties?.request; + if (request) { + defs.push({ + file: "request.md", + title: request.title || "Request", + description: request.description, + schema: request, + mode: "direct", + }); + + // request.headers (anyOf object variant) + const reqHeaders = request.properties?.headers; + if (reqHeaders?.anyOf) { + const obj = reqHeaders.anyOf.find( + (i: SchemaLike) => i.type === "object" + ); + if (obj) { + defs.push({ + file: "request-headers-object.md", + title: obj.title || "Request headers (object)", + description: obj.description, + schema: obj, + mode: "empty", + }); + } + } + + // request.parameters + const reqParams = request.properties?.parameters; + if (reqParams) { + defs.push({ + file: "request-parameters.md", + title: reqParams.title || "Request parameters", + description: reqParams.description, + schema: reqParams, + mode: "empty", + }); + } + + // request.body (anyOf object variant) + const reqBody = request.properties?.body; + if (reqBody?.anyOf) { + const obj = reqBody.anyOf.find( + (i: SchemaLike) => i.type === "object" + ); + if (obj) { + defs.push({ + file: "request-body-object.md", + title: obj.title || "Request body (object)", + description: obj.description, + schema: obj, + mode: "empty", + }); + } + } + } + + // response + const response = hrObj.properties?.response; + if (response) { + defs.push({ + file: "response.md", + title: response.title || "Response", + description: response.description, + schema: response, + mode: "direct", + }); + + // response.headers + const respHeaders = response.properties?.headers; + if (respHeaders) { + defs.push({ + file: "response-headers.md", + title: respHeaders.title || "Response headers", + description: respHeaders.description, + schema: respHeaders, + mode: "empty", + }); + } + + // response.body (anyOf object variant) + const respBody = response.properties?.body; + if (respBody?.anyOf) { + const obj = respBody.anyOf.find( + (i: SchemaLike) => i.type === "object" + ); + if (obj) { + defs.push({ + file: "response-body-object.md", + title: obj.title || "Response body object", + description: obj.description, + schema: obj, + mode: "empty", + }); + } + } + } + } + + // ── screenshot sub-schemas ───────────────────────────────────────────── + { + const cropEl = allSchemas.screenshot_v3?.components?.schemas?.crop_element; + if (cropEl) { + defs.push({ + file: "crop-by-element-detailed.md", + title: cropEl.title || "Crop by element (detailed)", + description: cropEl.description, + schema: cropEl, + mode: "direct", + }); + } + + const padding = allSchemas.screenshot_v3?.components?.schemas?.padding; + if (padding) { + defs.push({ + file: "padding-detailed.md", + title: padding.title || "Padding (detailed)", + description: padding.description, + schema: padding, + mode: "direct", + }); + } + } + + // ── find sub-schemas ─────────────────────────────────────────────────── + { + const findObj = comp("find_v3", "object"); + const clickProp = findObj.properties?.click; + if (clickProp?.anyOf) { + const feClick = clickProp.anyOf.find( + (i: SchemaLike) => i.title === "Find element and click" + ); + if (feClick) { + defs.push({ + file: "find-element-and-click.md", + title: feClick.title!, + description: feClick.description, + schema: feClick, + mode: "direct", + }); + } + } + } + + // ── resolved context ─────────────────────────────────────────────────── + { + const test = get("test_v3"); + const ctxItems = test.properties?.contexts?.items; + if (ctxItems) { + defs.push({ + file: "resolved-context.md", + title: "Resolved context", + description: ctxItems.description, + schema: ctxItems, + mode: "direct", + }); + } + } + + return defs; +} + +// --------------------------------------------------------------------------- +// Type rendering +// --------------------------------------------------------------------------- + +/** + * Check if an anyOf/oneOf is just conditional validation (different required + * field combos) rather than actual type alternatives. + */ +function isConditionalValidation(variants: SchemaLike[]): boolean { + return variants.every( + (v) => + v.required && + !v.type && + !v.anyOf && + !v.oneOf && + !v.properties + ); +} + +/** Render a JSON Schema type as markdown inline text */ +function renderType(schema: SchemaLike, depth = 0): string { + if (!schema) return "unknown"; + + // Object type with title → always render as object reference (even if it has anyOf for validation) + if (schema.type === "object" && schema.title) { + const slug = titleToFileMap.get(schema.title); + if (slug) { + const ref = slug.replace(".md", ""); + return `object([${schema.title}](/reference/schemas/${ref}))`; + } + return `object(${schema.title})`; + } + + // Object type without title but with anyOf that is just conditional validation + if ( + schema.type === "object" && + schema.anyOf && + isConditionalValidation(schema.anyOf) + ) { + return "object"; + } + + // anyOf / oneOf (type alternatives) + const variants = schema.anyOf || schema.oneOf; + if (variants && variants.length > 0 && !isConditionalValidation(variants)) { + // Flatten nested anyOf/oneOf (e.g., click_v3 anyOf containing another anyOf) + const flatVariants: SchemaLike[] = []; + for (const v of variants) { + const inner = v.anyOf || v.oneOf; + if (inner && inner.length > 0 && !isConditionalValidation(inner)) { + flatVariants.push(...inner); + } else { + flatVariants.push(v); + } + } + const rendered = [...new Set( + flatVariants + .map((v) => renderType(v, depth + 1)) + .filter((t) => t !== "unknown") + )]; + if (rendered.length === 0) return "unknown"; + if (rendered.length === 1) return rendered[0]; + return "one of:
" + rendered.map((t) => `- ${t}`).join("
"); + } + + // allOf – mostly just pass through the first meaningful one + if (schema.allOf && schema.allOf.length > 0) { + return renderType(schema.allOf[0], depth); + } + + // Array type + if (schema.type === "array") { + if (schema.items) { + const itemType = renderType(schema.items, depth + 1); + return `array of ${itemType}`; + } + return "array of unknown"; + } + + // Multi-type (e.g., ["boolean", "null"]) + if (Array.isArray(schema.type)) { + return schema.type.filter((t) => t !== "null").join(","); + } + + // Object type (no title) + if (schema.type === "object") { + if ( + schema.properties && + Object.keys(schema.properties).length === 0 && + schema.additionalProperties + ) { + return "object"; + } + return "object"; + } + + // Primitive types + if (schema.type) return schema.type as string; + + // Schema reference (has title but no explicit type) + if (schema.title) { + const slug = titleToFileMap.get(schema.title); + if (slug) { + const ref = slug.replace(".md", ""); + return `object([${schema.title}](/reference/schemas/${ref}))`; + } + return "unknown"; + } + + return "unknown"; +} + +// --------------------------------------------------------------------------- +// Build "Referenced In" map +// --------------------------------------------------------------------------- +type RefMap = Map>; // file → set of referencing page files + +function buildReferenceMap(pageDefs: PageDef[]): RefMap { + const refMap: RefMap = new Map(); + + // Initialize + for (const pd of pageDefs) { + refMap.set(pd.file, new Set()); + } + + // Action wrapper page files + const actionWrapperFiles = new Set( + pageDefs.filter((pd) => pd.mode === "wrapper").map((pd) => pd.file) + ); + + // For each page, scan its rendered type strings for cross-references + for (const pd of pageDefs) { + const referencedFiles = findReferencedFiles(pd); + for (const refFile of referencedFiles) { + if (refMap.has(refFile)) { + refMap.get(refFile)!.add(pd.file); + } + } + } + + // Action wrappers are referenced by step-using pages: markup-definition, test, resolved-context + const stepParents = [ + "markup-definition.md", + "test.md", + "resolved-context.md", + ]; + for (const wrapperFile of actionWrapperFiles) { + const refs = refMap.get(wrapperFile)!; + for (const parent of stepParents) { + if (refMap.has(parent)) { + refs.add(parent); + } + } + } + + return refMap; +} + +/** Find which other pages a given page's fields reference */ +function findReferencedFiles(pd: PageDef): Set { + const refs = new Set(); + + if (pd.mode === "wrapper") { + // The wrapper type references the schema's type variants + scanSchemaForRefs(pd.schema, refs, 0); + } else if (pd.schema.properties) { + for (const prop of Object.values(pd.schema.properties)) { + scanSchemaForRefs(prop, refs, 0); + } + } + + return refs; +} + +function scanSchemaForRefs(schema: SchemaLike, refs: Set, depth: number) { + if (!schema || depth > 3) return; + + // Object with title → potential cross-reference to a page + if (schema.type === "object" && schema.title) { + const slug = titleToFileMap.get(schema.title); + if (slug) refs.add(slug); + return; // Don't recurse into the object's properties + } + + // Title without type → also a potential reference + if (schema.title && !schema.type) { + const slug = titleToFileMap.get(schema.title); + if (slug) refs.add(slug); + } + + if (schema.anyOf) + for (const s of schema.anyOf) scanSchemaForRefs(s, refs, depth + 1); + if (schema.oneOf) + for (const s of schema.oneOf) scanSchemaForRefs(s, refs, depth + 1); + if (schema.allOf) + for (const s of schema.allOf) scanSchemaForRefs(s, refs, depth + 1); + if (schema.items) scanSchemaForRefs(schema.items, refs, depth + 1); +} + +// --------------------------------------------------------------------------- +// Render a schema page to markdown +// --------------------------------------------------------------------------- + +function renderSchemaPage(pd: PageDef, refMap: RefMap): string { + const lines: string[] = []; + + // Frontmatter + lines.push("---"); + lines.push(`title: "${pd.title}"`); + lines.push("---"); + lines.push(""); + + // Description + if (pd.description) { + lines.push(pd.description); + lines.push(""); + } + + // Referenced In + const refs = refMap.get(pd.file); + if (refs && refs.size > 0) { + lines.push("## Referenced In"); + lines.push(""); + const sorted = [...refs].filter((r) => r !== pd.file).sort(); + for (const refFile of sorted) { + const refTitle = fileToTitleMap.get(refFile) || refFile; + const refSlug = refFile.replace(".md", ""); + lines.push(`- [${refTitle}](/reference/schemas/${refSlug})`); + } + lines.push(""); + } + + // Fields + lines.push("## Fields"); + lines.push(""); + lines.push("Field | Type | Description | Default"); + lines.push(":-- | :-- | :-- | :--"); + + if (pd.mode === "wrapper") { + // Single field row for step wrapper + const fieldName = pd.wrapField!; + const typeStr = renderWrapperType(pd.schema); + const desc = buildDescription(pd.schema, true); + const def = renderDefault(pd.schema); + lines.push(`${fieldName} | ${typeStr} | ${desc} | ${def}`); + } else if (pd.mode === "direct" && pd.schema.properties) { + // Only use top-level required array; anyOf required is conditional validation + const required = new Set(pd.schema.required || []); + + for (const [fieldName, fieldSchema] of Object.entries( + pd.schema.properties + )) { + const typeStr = renderType(fieldSchema); + const isRequired = required.has(fieldName); + const isReadOnly = !!fieldSchema.readOnly; + const desc = buildFieldDescription(fieldSchema, isRequired, isReadOnly); + const def = renderDefault(fieldSchema); + lines.push(`${fieldName} | ${typeStr} | ${desc} | ${def}`); + } + } + // "empty" mode: no field rows (just the table header) + + lines.push(""); + + // Examples + lines.push("## Examples"); + + const examples = pd.mode === "wrapper" + ? getWrapperExamples(pd) + : getDirectExamples(pd); + + if (examples.length > 0) { + for (const ex of examples) { + lines.push(""); + lines.push("```json"); + lines.push(JSON.stringify(ex, null, 2)); + lines.push("```"); + } + } + + // Ensure file ends with newline + return lines.join("\n") + "\n"; +} + +/** Render the type string for a step wrapper page's single field */ +function renderWrapperType(schema: SchemaLike): string { + // For schemas with anyOf at top level (type alternatives, not conditional validation) + if (schema.anyOf && !isConditionalValidation(schema.anyOf)) { + const parts: string[] = []; + for (const variant of schema.anyOf) { + const rendered = renderType(variant); + if (rendered !== "unknown") parts.push(rendered); + } + if (parts.length === 1) return parts[0]; + if (parts.length > 1) { + return "one of:
" + parts.map((p) => `- ${p}`).join("
"); + } + } + + // For direct type schemas (like loadVariables_v3 which is just "string") + if (Array.isArray(schema.type)) { + return schema.type.filter((t) => t !== "null").join(","); + } + if (schema.type === "object") { + // Self-referencing like dragAndDrop + const title = schema.title; + if (title) { + const slug = titleToFileMap.get(title); + if (slug) { + const ref = slug.replace(".md", ""); + return `object([${title}](/reference/schemas/${ref}))`; + } + } + return "object"; + } + if (schema.type) return schema.type as string; + return "unknown"; +} + +function buildDescription( + schema: SchemaLike, + isRequired: boolean +): string { + const parts: string[] = []; + parts.push(isRequired ? "Required." : "Optional."); + + if (schema.description) { + parts.push(schema.description); + } else { + parts.push("No description provided."); + } + + return parts.join(" "); +} + +function buildFieldDescription( + schema: SchemaLike, + isRequired: boolean, + isReadOnly: boolean +): string { + const parts: string[] = []; + + if (isReadOnly) { + parts.push("ReadOnly."); + } else { + parts.push(isRequired ? "Required." : "Optional."); + } + + if (schema.description) { + parts.push(schema.description); + } else { + parts.push("No description provided."); + } + + // Build suffix separately to avoid extra spaces before
+ const suffixes: string[] = []; + + // Pattern + if (schema.pattern) { + // Clean up pattern for display - remove extra escapes + const displayPattern = schema.pattern + .replace(/\\\\/g, "\\") + .replace(/^\^/, "") + .replace(/\$$/, ""); + suffixes.push(`Pattern: \`${displayPattern}\``); + } + + // Accepted values (enum) + if (schema.enum && schema.enum.length > 0) { + const vals = schema.enum.map((v) => `\`${v}\``).join(", "); + suffixes.push(`Accepted values: ${vals}`); + } + + // Minimum / Maximum + if (schema.minimum !== undefined || schema.maximum !== undefined) { + const constraints: string[] = []; + if (schema.minimum !== undefined) + constraints.push(`Minimum: ${schema.minimum}`); + if (schema.maximum !== undefined) + constraints.push(`Maximum: ${schema.maximum}`); + suffixes.push(constraints.join(". ")); + } + + let result = parts.join(" "); + if (suffixes.length > 0) { + result += "

" + suffixes.join("

"); + } + return result; +} + +function renderDefault(schema: SchemaLike): string { + if (schema.default === undefined) return ""; + if (schema.default === null) return "`null`"; + if (typeof schema.default === "string") return `\`${schema.default}\``; + if (typeof schema.default === "boolean") + return `\`${schema.default}\``; + if (typeof schema.default === "number") + return `\`${schema.default}\``; + // Complex defaults (arrays, objects) + return `\`\`${JSON.stringify(schema.default)}\`\``; +} + +/** Get examples for wrapper pages - wrap each example in {actionName: example} */ +function getWrapperExamples(pd: PageDef): unknown[] { + const schema = pd.schema; + const fieldName = pd.wrapField!; + + if (schema.examples && schema.examples.length > 0) { + return schema.examples.map((ex) => ({ [fieldName]: ex })); + } + + // Fallback: generate a simple example + return [{ [fieldName]: generateExample(schema) }]; +} + +/** Get examples for direct schema pages */ +function getDirectExamples(pd: PageDef): unknown[] { + const schema = pd.schema; + + if (schema.examples && schema.examples.length > 0) { + return schema.examples as unknown[]; + } + + // Fallback: generate from properties + if (schema.properties) { + const ex: Record = {}; + for (const [name, prop] of Object.entries(schema.properties)) { + ex[name] = generateExample(prop); + } + return [ex]; + } + + return []; +} + +function generateExample(schema: SchemaLike): unknown { + if (schema.default !== undefined) return schema.default; + if (schema.enum && schema.enum.length > 0) return schema.enum[0]; + if (schema.examples && schema.examples.length > 0) + return schema.examples[0]; + + const type = Array.isArray(schema.type) ? schema.type[0] : schema.type; + switch (type) { + case "string": + return "example"; + case "integer": + case "number": + return 42; + case "boolean": + return true; + case "array": + return []; + case "object": + return {}; + default: + if (schema.anyOf) return generateExample(schema.anyOf[0]); + return "example"; + } +} + +// --------------------------------------------------------------------------- +// Phase 2: API function page generation +// --------------------------------------------------------------------------- + +interface FunctionSig { + name: string; + params: Array<{ name: string; type: string; required: boolean }>; + returnType: string; + jsdoc?: string; + isAsync: boolean; +} + +function parseTsFunctions(): FunctionSig[] { + const functions: FunctionSig[] = []; + + const files = [ + path.join(SOURCE_REPO, "src/core/index.ts"), + path.join(SOURCE_REPO, "src/core/tests.ts"), + path.join(SOURCE_REPO, "src/core/detectTests.ts"), + path.join(SOURCE_REPO, "src/core/resolveTests.ts"), + path.join(SOURCE_REPO, "src/core/files.ts"), + ]; + + const exportedNames = new Set([ + "runTests", + "getRunner", + "detectTests", + "detectAndResolveTests", + "resolveTests", + "readFile", + "resolvePaths", + ]); + + for (const filePath of files) { + if (!fs.existsSync(filePath)) continue; + + const sourceText = fs.readFileSync(filePath, "utf8"); + const sourceFile = ts.createSourceFile( + filePath, + sourceText, + ts.ScriptTarget.Latest, + true + ); + + ts.forEachChild(sourceFile, (node) => { + if (ts.isFunctionDeclaration(node) && node.name) { + const name = node.name.text; + if (!exportedNames.has(name)) return; + + const sig = extractFunctionSig(node, sourceFile); + if (sig) functions.push(sig); + } + }); + } + + return functions; +} + +function extractFunctionSig( + node: ts.FunctionDeclaration, + sourceFile: ts.SourceFile +): FunctionSig | null { + const name = node.name?.text; + if (!name) return null; + + const isAsync = !!node.modifiers?.some( + (m) => m.kind === ts.SyntaxKind.AsyncKeyword + ); + + const params: FunctionSig["params"] = []; + + for (const param of node.parameters) { + const paramName = param.name.getText(sourceFile); + const paramType = param.type + ? param.type.getText(sourceFile) + : "any"; + const required = !param.questionToken && !param.initializer; + + // Handle destructured params + if (ts.isObjectBindingPattern(param.name)) { + // Extract from type annotation if available + if (param.type && ts.isTypeLiteralNode(param.type)) { + for (const member of param.type.members) { + if (ts.isPropertySignature(member) && member.name) { + const mName = member.name.getText(sourceFile); + const mType = member.type + ? member.type.getText(sourceFile) + : "any"; + const mRequired = !member.questionToken; + params.push({ name: mName, type: mType, required: mRequired }); + } + } + } else { + // Expand binding elements individually (type may be a reference or any) + for (const element of param.name.elements) { + if (ts.isBindingElement(element)) { + const eName = element.name.getText(sourceFile); + const eRequired = !element.dotDotDotToken && !element.initializer; + params.push({ name: eName, type: paramType, required: eRequired }); + } + } + } + } else { + params.push({ name: paramName, type: paramType, required }); + } + } + + const returnType = node.type + ? node.type.getText(sourceFile) + : "any"; + + // Extract JSDoc + let jsdoc: string | undefined; + const jsDocNodes = ts.getJSDocCommentsAndTags(node); + for (const doc of jsDocNodes) { + if (ts.isJSDoc(doc) && doc.comment) { + jsdoc = + typeof doc.comment === "string" + ? doc.comment + : doc.comment + .map((c) => ("text" in c ? c.text : "")) + .join(""); + } + } + + return { name, params, returnType, jsdoc, isAsync }; +} + +// --------------------------------------------------------------------------- +// API page generation +// --------------------------------------------------------------------------- + +const CUSTOM_START = "{/* CUSTOM CONTENT START */}"; +const CUSTOM_END = "{/* CUSTOM CONTENT END */}"; + +interface ApiPageDef { + file: string; + functionName: string; + sidebarTitle?: string; +} + +const API_PAGES: ApiPageDef[] = [ + { file: "run-tests.mdx", functionName: "runTests" }, + { file: "get-runner.mdx", functionName: "getRunner" }, + { file: "detect-tests.mdx", functionName: "detectTests" }, + { + file: "detect-and-resolve-tests.mdx", + functionName: "detectAndResolveTests", + }, + { file: "resolve-tests.mdx", functionName: "resolveTests" }, + { file: "read-file.mdx", functionName: "readFile" }, + { file: "resolve-paths.mdx", functionName: "resolvePaths" }, +]; + +function generateApiPage(apd: ApiPageDef, sig: FunctionSig | undefined): string { + const existingPath = path.join(API_OUT_DIR, apd.file); + let customContent = ""; + + // Preserve existing custom content between markers + if (fs.existsSync(existingPath)) { + const existing = fs.readFileSync(existingPath, "utf8"); + const startIdx = existing.indexOf(CUSTOM_START); + const endIdx = existing.indexOf(CUSTOM_END); + if (startIdx !== -1 && endIdx !== -1) { + customContent = existing.substring( + startIdx + CUSTOM_START.length, + endIdx + ); + } else { + // No markers yet – preserve everything after the generated sections + // Find where custom content starts (after ## Return value section) + const returnIdx = existing.indexOf("## Return value"); + if (returnIdx !== -1) { + // Find end of return value section (next ## or end) + const afterReturn = existing.substring(returnIdx); + const nextSection = afterReturn.indexOf("\n## ", 1); + if (nextSection !== -1) { + customContent = + "\n" + afterReturn.substring(nextSection).trimEnd() + "\n"; + } + } else { + // Preserve everything after frontmatter and first paragraph + const fmEnd = existing.indexOf("---", 4); + if (fmEnd !== -1) { + const afterFm = existing.substring(fmEnd + 3); + customContent = "\n" + afterFm.trimEnd() + "\n"; + } + } + } + } + + const lines: string[] = []; + + // Frontmatter + lines.push("---"); + lines.push(`title: ${apd.functionName}()`); + if (sig?.jsdoc) { + lines.push(`description: ${sig.jsdoc.split("\n")[0]}`); + } + lines.push("---"); + lines.push(""); + + if (sig) { + // Signature + const paramStr = sig.params + .map((p) => `${p.name}${p.required ? "" : "?"}`) + .join(", "); + const retType = sig.returnType.replace(/Promise<(.+)>/, "$1"); + lines.push("## Signature"); + lines.push(""); + lines.push("```javascript"); + lines.push( + `${sig.isAsync ? "async " : ""}${sig.name}(${paramStr}) → ${retType}` + ); + lines.push("```"); + lines.push(""); + + // Parameters + if (sig.params.length > 0) { + lines.push("## Parameters"); + lines.push(""); + lines.push( + "| Parameter | Type | Required | Description |" + ); + lines.push("|-----------|------|----------|-------------|"); + for (const p of sig.params) { + lines.push( + `| \`${p.name}\` | ${p.type} | ${p.required ? "Yes" : "No"} | |` + ); + } + lines.push(""); + } + + // Return value + lines.push("## Return value"); + lines.push(""); + lines.push( + `Returns \`${sig.returnType}\`.` + ); + lines.push(""); + } + + // Custom content + lines.push(CUSTOM_START); + lines.push(customContent || ""); + lines.push(CUSTOM_END); + + return lines.join("\n") + "\n"; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +function main() { + console.log("Building page definitions..."); + const pageDefs = buildPageDefs(); + + // Register all pages for cross-reference resolution + for (const pd of pageDefs) { + registerPage(pd.title, pd.file); + } + + // Also register known titles that map to pages via schema components + // (for object types referenced in field types) + registerExtraComponentTitles(pageDefs); + + console.log(`Registered ${titleToFileMap.size} page titles`); + + // Build reference map + console.log("Building cross-reference map..."); + const refMap = buildReferenceMap(pageDefs); + + // Generate schema pages + console.log(`Generating ${pageDefs.length} schema pages...`); + fs.mkdirSync(SCHEMA_OUT_DIR, { recursive: true }); + + for (const pd of pageDefs) { + const content = renderSchemaPage(pd, refMap); + const outPath = path.join(SCHEMA_OUT_DIR, pd.file); + fs.writeFileSync(outPath, content, "utf8"); + } + + console.log("Schema pages generated."); + + // Parse TS functions and generate API pages + console.log("Parsing TypeScript function signatures..."); + const functions = parseTsFunctions(); + console.log(`Found ${functions.length} functions`); + + const funcMap = new Map(functions.map((f) => [f.name, f])); + + fs.mkdirSync(API_OUT_DIR, { recursive: true }); + + for (const apd of API_PAGES) { + const sig = funcMap.get(apd.functionName); + const content = generateApiPage(apd, sig); + const outPath = path.join(API_OUT_DIR, apd.file); + fs.writeFileSync(outPath, content, "utf8"); + if (!sig) { + console.warn(` Warning: No function signature found for ${apd.functionName}`); + } + } + + console.log("API pages generated."); + console.log("Done!"); +} + +/** Register extra title→file mappings for component schemas */ +function registerExtraComponentTitles(pageDefs: PageDef[]) { + // Walk all v3 schemas and register component schema titles + const v3keys = Object.keys(allSchemas).filter((k) => k.endsWith("_v3")); + for (const key of v3keys) { + const schema = allSchemas[key]; + if (schema.components?.schemas) { + for (const [, compSchema] of Object.entries( + schema.components.schemas + )) { + if (compSchema.title && !titleToFileMap.has(compSchema.title)) { + // Try to find a matching registered page + const slug = titleToSlug(compSchema.title); + const possibleFile = slug + ".md"; + // Only register if we actually have this page + if (fileToTitleMap.has(possibleFile)) { + titleToFileMap.set(compSchema.title, possibleFile); + } + } + } + } + } + + // Walk nested properties for titles + for (const pd of pageDefs) { + if (pd.schema.properties) { + for (const prop of Object.values(pd.schema.properties)) { + registerNestedTitles(prop); + } + } + } +} + +function registerNestedTitles(schema: SchemaLike) { + if (!schema) return; + + if (schema.title && !titleToFileMap.has(schema.title)) { + const slug = titleToSlug(schema.title); + const possibleFile = slug + ".md"; + if (fileToTitleMap.has(possibleFile)) { + titleToFileMap.set(schema.title, possibleFile); + } + } + + if (schema.anyOf) for (const s of schema.anyOf) registerNestedTitles(s); + if (schema.oneOf) for (const s of schema.oneOf) registerNestedTitles(s); + if (schema.allOf) for (const s of schema.allOf) registerNestedTitles(s); + if (schema.items) registerNestedTitles(schema.items); +} + +main();