Skip to content

Output ​

A scan returns one JSON object. Its JSON Schema is published with each release.

json
{
  "schemaVersion": 1,
  "tool": { "name": "test-inventory", "version": "0.1.0" },
  "framework": "playwright",
  "frameworkVersion": "1.63.0",
  "summary": { "fileCount": 11, "testCount": 31 },
  "suites": [],
  "tests": [],
  "diagnostics": []
}

Rules for every key ​

  • Every key is always present, in every record, for both frameworks. Nothing is undefined or left out.
  • A boolean that cannot be true in a framework is false: isFixme in Vitest, isShuffled in Playwright.
  • A list with nothing in it is [].
  • A value that does not exist or could not be worked out is null.
  • Two scans of the same code give the same output, byte for byte. There is no timestamp.
  • Paths are relative to the project root and use / on every system.
  • Lines and columns start at 1. A line ends at \n, \r\n or \r, and columns count UTF-16 code units, as editors do.

In the TypeScript declarations on this page, ids and hashes are a Hash, lines and columns a Position, and counts a Count:

Hash, Position and Count
ts
/** @pattern ^[0-9a-f]{16}$ */
type Hash = string;

/**
 * @asType integer
 * @minimum 1
 */
type Position = number;

/**
 * @asType integer
 * @minimum 0
 */
type Count = number;

Top level ​

KeyTypeMeaning
schemaVersion1The version of this format. See versioning.
toolobjectname is test-inventory; version is the version that produced the output.
frameworkstringplaywright or vitest, as given to the scan.
frameworkVersionstring or nullRead from the project, or given with --framework-version. See framework version.
summaryobjectCounts over the whole scan.
suitesarrayOne record per describe.
testsarrayOne record per test declaration.
diagnosticsarrayWhat the scan could not read, or read only in part.
TypeScript
ts
/** The output of one scan. */
export interface Inventory {
  /** Version of this format. It changes only when a key is removed, renamed or changes meaning. */
  schemaVersion: 1;
  tool: Tool;
  /** The framework the scan was run for. */
  framework: Framework;
  /** Read from the project, or given with `--framework-version`; null when unknown. */
  frameworkVersion: string | null;
  summary: Summary;
  suites: SuiteRecord[];
  tests: TestRecord[];
  diagnostics: Diagnostic[];
}

export interface Tool {
  name: 'test-inventory';
  /** Version of test-inventory that produced the output. */
  version: string;
}

export type Framework = 'playwright' | 'vitest';

Tests ​

A test record holds the values the runner applies to the test, including those it inherits from the describes and the file around it.

KeyTypeMeaning
idstring16 hex characters from the file path, the describe titles and title without tags, and the position among tests with the same title. It stays the same when the test moves to another line.
bodyHashstring or null16 hex characters from the test function's syntax tree, so formatting, comments, quote style, semicolons, trailing commas and line endings don't change it. For a function passed by name, the hash of that function when it is declared in the file. null without a function. Trivial bodies such as async () => {} share a hash.
suiteIdstring or nullThe nearest describe. null outside any describe.
suitePathstring[]Titles of the describes around the test, outermost first.
relativeFilePathstringThe file, from the project root.
fileNamestringThe file name without folders.
lineStart, lineEndnumberFirst and last line of the test call.
columnStart, columnEndnumberWhere the test call starts, and the column just after its last character.
titlestringThe title as the runner shows it, tags included.
titleWithoutTagsstringPlaywright: the title without its @tag words. Vitest: the title.
hasDynamicTitlebooleanThe title could not be worked out; title holds its source text.
fullNamestringsuitePath and title joined with >.
isTypeTestbooleanDeclared in a Vitest type test file, *.test-d.ts or *.spec-d.ts.
statestringactive, skip, fixme, todo, expectedToFail or notLoaded.
isActive, isSkipped, isFixme, isTodo, isExpectedToFail, isNotLoadedbooleanExactly one is true, matching state.
isOnlybooleanThe test or a describe around it uses .only.
isConditionalbooleanThe state applies only when a condition holds.
conditionstring or nullThe condition's source text, on one line and cut at 200 characters.
reasonstring or nullThe reason given with the state.
hasDynamicReasonbooleanThe reason could not be worked out to a string; reason holds its source text.
stateSourcestring or nullWhere the state is set: test, suite or file. null for active tests.
stateLinenumber or nullThe line that sets the state. null for active tests.
isParallel, isSerialbooleanThe mode set in the file. Both false means the framework's default.
tagsstring[]Tags of the test and every describe around it, outermost first, without duplicates.
annotationsobject[]{ type, description, lineStart } from Playwright's annotation option and test.info().annotations.push().
commentsstring[]The comment lines directly above the test, without lint, compiler, formatter and coverage directives. By default only for tests that are not active.
locksstring[]Playwright lock names on the test and its describes (1.63), and from test.describe.configure() in the file and those describes (1.64).
metaobject or nullVitest meta, merged from the describes and the test, literal values only.
isParameterizedbooleanDeclared with .each or .for, or inside a describe.each or describe.for, so the runner repeats it.
caseCountnumber or nullHow many times the runner repeats the test: its own cases times those of each describe table around it. null when a table is not written out in the file, or the test is not parameterized.
isInLoopbooleanDeclared inside a loop, forEach or map.
isInConditionbooleanDeclared inside if, ?:, &&, ||, ?? or switch.
isInFunctionbooleanDeclared inside a plain function. When calls of the function would place it elsewhere, a test-in-function warning says so.
TypeScript
ts
/** A test, with the effective values the runner applies, including those inherited from its describes. */
export interface TestRecord {
  /** Derived from the file path, the describe titles, the title and its position among tests with the same title. */
  id: Hash;
  /** Hash of the test body, ignoring whitespace and comments; null when the test has no body. */
  bodyHash: Hash | null;
  /** The nearest describe; null outside any describe. */
  suiteId: Hash | null;
  /** Titles of the describes around the test, outermost first. */
  suitePath: string[];
  /** Path from the project root, with `/` separators. */
  relativeFilePath: string;
  fileName: string;
  lineStart: Position;
  lineEnd: Position;
  /** In UTF-16 code units. */
  columnStart: Position;
  /** In UTF-16 code units. */
  columnEnd: Position;
  /** As the runner shows it, tags included. */
  title: string;
  /** `title` with `@tag` words removed and spaces tidied. */
  titleWithoutTags: string;
  /** The title could not be resolved to a fixed string, so `title` holds its source text. */
  hasDynamicTitle: boolean;
  /** `suitePath` and `title` joined with " > ". */
  fullName: string;
  /** Declared in a Vitest type test file (`*.test-d.ts`, `*.spec-d.ts`). */
  isTypeTest: boolean;
  state: TestState;
  isActive: boolean;
  isSkipped: boolean;
  isFixme: boolean;
  isTodo: boolean;
  isExpectedToFail: boolean;
  /** The runner refuses to load the file; a diagnostic at `stateLine` says why. */
  isNotLoaded: boolean;
  /** The test or a describe around it uses `.only`. */
  isOnly: boolean;
  /** The state applies only when a condition holds, as with `test.skip(condition)` or `skipIf`. */
  isConditional: boolean;
  /** Source text of the condition, shortened when long; null when unconditional. */
  condition: string | null;
  reason: string | null;
  /** The reason could not be worked out to a string, so `reason` holds its source text. */
  hasDynamicReason: boolean;
  /** null for active tests. */
  stateSource: StateSource | null;
  /** The line that sets the state; null for active tests. */
  stateLine: Position | null;
  /** Both `isParallel` and `isSerial` false means the framework's default mode. */
  isParallel: boolean;
  isSerial: boolean;
  /** From the test and every describe around it, outermost first, without duplicates. */
  tags: string[];
  annotations: Annotation[];
  /** Comment lines directly above the test. By default only filled for tests that are not active. */
  comments: string[];
  /** From the test, its describes and the `test.describe.configure()` calls around it. Playwright only. */
  locks: string[];
  /** Merged from the describes and the test. Vitest only; literal values only. */
  meta: Record<string, MetaValue> | null;
  /** Declared with `.each` or `.for`, or inside a describe declared with them, so the runner repeats it. */
  isParameterized: boolean;
  /**
   * How many times the runner repeats the test: the cases of its own table times those of each describe table around
   * it. Null when a table is not written out in the file, or the test is not parameterized.
   */
  caseCount: Count | null;
  /** Declared inside a loop, `forEach` or `map`. */
  isInLoop: boolean;
  /** Declared inside `if`, `?:`, `&&` or `switch`. */
  isInCondition: boolean;
  /** Declared inside a plain function. */
  isInFunction: boolean;
}

/**
 * The state the runner applies to a test, after describes and file-level calls are taken into account. `notLoaded`:
 * the runner refuses to load the file, so none of its tests run.
 */
export type TestState = 'active' | 'skip' | 'fixme' | 'todo' | 'expectedToFail' | 'notLoaded';

/** Where a test's state is set: on the test, on a describe around it, or at file level. */
export type StateSource = 'test' | 'suite' | 'file';

export interface Annotation {
  type: string;
  description: string | null;
  lineStart: Position;
}

export type MetaValue = string | number | boolean | null | MetaValue[] | { [key: string]: MetaValue };

How the state is chosen ​

The state is the one the runner applies. The test's own skip, fixme or todo comes first: test.fixme() in a skipped describe stays fixme. Then the describes around it from the innermost out, then the file. A state that always applies wins over one that only applies under a condition, and an unconditional skip, fixme or todo around a test wins over the test's own expected failure, as neither runner runs that test. stateSource says where the chosen state was set.

In Vitest, .only wins over .skip, which wins over .todo, within one declaration: test.only.skip is active.

A test.skip(), test.fixme(), test.fail() or test.describe.configure() call inside a plain function in the file applies where the function is called: to a test that calls it in its body, to the tests a hook covers when a hook calls it, or to a describe when it is called directly in the describe's body. The conditions around the call and inside the function are joined, and stateLine is the line of the call inside the function. Calls through other functions in the file are followed too. A function that is exported, or that nothing in the file calls, changes no record and gets a state-in-helper warning.

When a file has a problem that makes the runner refuse to load it, such as a Playwright test without a body or a Vitest call to an API that the version removed, every test in it is notLoaded: stateSource is file, stateLine is the line of the first such problem, and condition and reason are null. The diagnostic says why. Everything else about the tests stays as written, so fixing the file brings their states back.

Suites ​

A suite record describes what is written on the describe itself. Values a test inherits from it are in the test record.

KeyTypeMeaning
id, parentIdstring, string or nullThe describe and the one around it.
bodyHashstring or nullThe describe's own code: hooks, test.use(), describe.configure() and helpers, without the tests and describes inside it, hashed as for tests. null when nothing is left.
relativeFilePath, fileName, lineStart, lineEnd, columnStart, columnEndAs for tests.
title, titleWithoutTags, hasDynamicTitleAs for tests. An anonymous Playwright describe has an empty title and is left out of paths.
pathstring[]The titles of this describe and those around it.
isSkipped, isFixme, isTodo, isExpectedToFail, isOnlybooleanSet on this describe. isExpectedToFail is a Playwright test.fail() in the describe's body.
isConditional, condition, reason, hasDynamicReasonAs for tests.
isParallel, isSerial, isShuffledbooleanThe mode set on this describe.
isParameterizedbooleandescribe.each or describe.for.
tags, annotations, locks, metaWritten on this describe.
TypeScript
ts
/** A describe block, with what is written on the describe itself. Inherited values are on the tests. */
export interface SuiteRecord {
  id: Hash;
  /** The describe around this one; null at the top level. */
  parentId: Hash | null;
  /**
   * Hash of the describe's own code (hooks, `test.use()`, `describe.configure()`, helpers), ignoring whitespace,
   * comments, and nested tests and describes; null when nothing is left.
   */
  bodyHash: Hash | null;
  /** Path from the project root, with `/` separators. */
  relativeFilePath: string;
  fileName: string;
  lineStart: Position;
  lineEnd: Position;
  /** In UTF-16 code units. */
  columnStart: Position;
  /** In UTF-16 code units. */
  columnEnd: Position;
  /** As the runner shows it, tags included. */
  title: string;
  /** `title` with `@tag` words removed and spaces tidied. */
  titleWithoutTags: string;
  /** The title could not be resolved to a fixed string, so `title` holds its source text. */
  hasDynamicTitle: boolean;
  /** Titles of the describes around this one and of this one, outermost first. */
  path: string[];
  isSkipped: boolean;
  /** Playwright only. */
  isFixme: boolean;
  /** Vitest only. */
  isTodo: boolean;
  /** Tests in the describe are expected to fail, as with `test.fail()` in its body. Playwright only. */
  isExpectedToFail: boolean;
  isOnly: boolean;
  /** The state applies only when a condition holds. */
  isConditional: boolean;
  /** Source text of the condition, shortened when long; null when unconditional. */
  condition: string | null;
  reason: string | null;
  /** The reason could not be worked out to a string, so `reason` holds its source text. */
  hasDynamicReason: boolean;
  isParallel: boolean;
  /** Playwright only. */
  isSerial: boolean;
  /** Vitest only. */
  isShuffled: boolean;
  /** Declared with `.each` or `.for`. */
  isParameterized: boolean;
  tags: string[];
  annotations: Annotation[];
  /** Playwright only. */
  locks: string[];
  /** Vitest only; literal values only. */
  meta: Record<string, MetaValue> | null;
}

Summary ​

Each count uses the same definition as the matching key: skipCount counts tests with isSkipped: true.

fileCount, suiteCount, testCount, activeCount, skipCount, fixmeCount, todoCount, expectedToFailCount, notLoadedCount, onlyCount, conditionalCount, parallelCount, serialCount, parameterizedCount, inLoopCount, inConditionCount, inFunctionCount, typeTestCount, diagnosticCount.

tagCounts maps each tag to the number of tests with it. annotationCounts maps each annotation type to the number of tests with at least one annotation of that type. Both are sorted by name.

TypeScript
ts
/** Counts over the whole scan. Each test count uses the same definition as the matching test key. */
export interface Summary {
  /** Test files scanned. */
  fileCount: Count;
  suiteCount: Count;
  testCount: Count;
  activeCount: Count;
  skipCount: Count;
  fixmeCount: Count;
  todoCount: Count;
  expectedToFailCount: Count;
  notLoadedCount: Count;
  onlyCount: Count;
  conditionalCount: Count;
  parallelCount: Count;
  serialCount: Count;
  parameterizedCount: Count;
  inLoopCount: Count;
  inConditionCount: Count;
  inFunctionCount: Count;
  typeTestCount: Count;
  diagnosticCount: Count;
  /** Tests per tag. */
  tagCounts: Record<string, Count>;
  /** Tests per annotation type. */
  annotationCounts: Record<string, Count>;
}

Diagnostics ​

KeyTypeMeaning
codestringOne of the diagnostic codes.
levelstringerror when tests may be missing; warning when a value is missing or approximate.
messagestringOne sentence about the problem.
relativeFilePathstringThe file.
lineStart, columnStartnumber or nullWhere the problem is. null for the whole file.
testIdstring or nullThe test it concerns.
sourcestring or nullThe code involved, cut at 200 characters.
TypeScript
ts
export interface Diagnostic {
  code: DiagnosticCode;
  level: DiagnosticLevel;
  /** One sentence describing the problem. */
  message: string;
  relativeFilePath: string;
  /** null for problems with the whole file. */
  lineStart: Position | null;
  /** null for problems with the whole file. */
  columnStart: Position | null;
  /** The affected test; null when none. */
  testId: Hash | null;
  /** Source text involved, shortened when long; null when none. */
  source: string | null;
}

/** `error`: tests may be missing from the output. `warning`: a value is missing or approximate. */
export type DiagnosticLevel = 'error' | 'warning';

export type DiagnosticCode =
  | 'unresolved-test-import'
  | 'parse-error'
  | 'read-error'
  | 'framework-mismatch'
  | 'unresolved-tag'
  | 'unresolved-annotation'
  | 'unresolved-cases'
  | 'test-in-loop'
  | 'dynamic-title'
  | 'duplicate-title'
  | 'duplicate-id'
  | 'internal-error'
  | 'invalid-chain'
  | 'skip-without-body'
  | 'test-without-body'
  | 'describe-not-called'
  | 'async-describe'
  | 'nested-test'
  | 'tag-without-at'
  | 'removed-api'
  | 'only-in-skipped-describe'
  | 'config-may-change-state'
  | 'symlink-ignored'
  | 'invalid-import'
  | 'globals-not-enabled'
  | 'unknown-framework-version'
  | 'unsupported-framework-version'
  | 'local-test-object'
  | 'state-in-helper'
  | 'unresolved-option'
  | 'dynamic-reason'
  | 'no-files-matched'
  | 'unsupported-file'
  | 'approximate-framework-version'
  | 'test-in-function';

Versioning ​

  • The package follows semantic versioning. Before 1.0, a breaking change comes in a minor release.
  • Adding a key, a diagnostic code or a value to an open set keeps schemaVersion. Read the output in a way that ignores keys you don't know; the published schema allows them.
  • These sets are open, so a reader should expect values it doesn't know yet: framework, state, stateSource and the diagnostic code. The published schema lists the known values and accepts any string.
  • Removing or renaming a key, or changing its type or meaning, raises schemaVersion by one and is a breaking release.
  • Each schemaVersion has its own schema file, shipped in the package as test-inventory/schema/v1.json and published on this site.
  • A key that is going away is marked deprecated in these pages and in the schema for at least one minor release first.