This document contains information and guidelines about contributing to this project. Please read it before you start participating.
Topics
A great way to contribute to the project is to send a detailed issue when you encounter an problem. I always appreciate a well-written, thorough bug report.
Check that the project issues database doesn't already include that problem or suggestion before submitting an issue. If you find a match, add a quick "+1" or "I have this problem too." Doing this helps prioritize the most common problems and requests.
When reporting issues, please include the following:
- The version of Xcode you're using
- The version of iOS or OS X you're targeting
- The full output of the command
- An archive of the resultBundlePath (Whenever possible)
- Any other details that would be useful in understanding the problem
This information will help review and fix your issue faster.
Pull requests are welcome, and greatly encouraged. When submitting a pull request, please add a description that explains what the changes are about. Link the ticket whenever possible.
- As usual, anyone can clone the repository. Then do the fixes/improvements as needed in own repository. When it is finished you can start the request to pull code from your own repo to XCTestHTMLReport repo.
The Swift test suite runs against real .xcresult bundles. Generate them once,
then run the tests:
./prepareTestResults.sh # builds the sample app and produces fixtures
swift testprepareTestResults.sh picks the newest available iPhone simulator automatically.
No credentials or secrets are required for this part.
If a simulator goes away mid-run, xcodebuild can leave an .xcresult behind
that looks complete but holds no tests, and the suite then fails with a
confusing error. A host-app launch failure does something subtler: the bundle is
full of tests, just not the ones hosted in the app.
scripts/verify_fixtures.sh — the same gate CI runs — catches both in a second:
./scripts/verify_fixtures.sh # asserts each bundle holds its expected test countIt knows the shape of each fixture (21, 1 and 4 tests for the three healthy ones), so a bundle that generated partially is rejected by name and count rather than waved through. Adding or removing sample-app tests moves those numbers: update the table at the top of the script in the same commit.
The fourth bundle, CrashResults.xcresult, is a broken run on purpose: the
script sets XCHR_TRAP_AT_LAUNCH, the sample app traps in
didFinishLaunchingWithOptions, and SampleAppUnitTests — hosted in that app
— dies with it. SystemFailureCanaryTests reads it to check that Apple still
calls the resulting group System Failures, which is the only handle the
systemFailure fault has. Expect one failing xcodebuild invocation during
generation; that is the fixture being made, not a problem.
Regenerate fixtures after upgrading Xcode; .xcresult contents change between
Xcode versions.
swift test also runs two other layers that need no .xcresult and no
simulator, because they render from a hand-written synthetic fixture instead:
-
Template snapshots —
TemplateSnapshotTestscompares rendered HTML against committed goldens inTests/XCTestHTMLReportTests/Snapshots/. If a template change is intentional, refresh the goldens and review the diff before committing:XCHR_UPDATE_SNAPSHOTS=1 swift test --filter TemplateSnapshotTestsA refresh run always fails on purpose (
SnapshotSupport.swift) — writing a golden proves nothing about whether the new content is correct. Re-run without the environment variable afterwards to get a real verdict. -
Browser assertions —
visual/at the repository root is a separate npm project (Playwright + axe-core) checking things only a browser knows: computed token values, WCAG contrast in light and dark mode, accessibility violations, and interactive behaviour. It reads a report file rather than building one, so first dump the synthetic fixture:XCHR_VISUAL_DIR="$(pwd)/visual/fixtures" swift test --filter VisualFixtureDumpTests cd visual npm ci npx playwright test
CI runs this as two jobs, mirrored above: a macOS job dumps the fixture, an ubuntu job installs Playwright's browsers and runs the assertions against it. See
docs/superpowers/specs/2026-08-13-report-visual-test-coverage-design.mdfor the full design and its layers.
CI runs prepareTestResults.sh, swift test, and the visual/ job above, so
a green run of all of it locally means a green run on your pull request.
CI runs shellcheck, SwiftFormat, and SwiftLint. The same checks can run locally on staged files, so you find problems before pushing.
mise installs the versions pinned in mise.toml — the
same ones CI uses, so a linter release cannot fail your pull request on a file
you did not touch — and, as a side effect, the pre-commit hook itself:
brew install mise # or https://mise.jdx.dev/installing-mise.html
mise trust && mise installThat is opt-in on purpose: nothing installs it for you, and mise trust is where
you agree to let the repository run its own setup.
The same file indexes the repository's runnable scripts: mise tasks lists
them (fixture generation and both comparison harnesses), and
mise run <task> -- <flags> runs one with its flags passed through.
The hook is defined in hk.pkl. It only checks files you are actually
committing, and reports problems rather than rewriting your files, so a commit
never contains changes you have not read. To skip it for one commit:
git commit --no-verifyIf you set up the older .githooks directory, it is gone now, and the
core.hooksPath it left behind will stop the new hook from firing. Clear it
once:
git config --local --unset-all core.hooksPath*Some of the ideas and wording for the statements above were based on AFNetworking.
