{"id":51043540,"url":"https://github.com/qoretechnologies/qlip","last_synced_at":"2026-06-22T12:01:42.520Z","repository":{"id":333759831,"uuid":"1138585691","full_name":"qoretechnologies/qlip","owner":"qoretechnologies","description":"A CLI project for capturing and filtering stories.","archived":false,"fork":false,"pushed_at":"2026-06-16T23:03:54.000Z","size":57856,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"develop","last_synced_at":"2026-06-17T01:07:32.527Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/qoretechnologies.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-01-20T21:32:12.000Z","updated_at":"2026-06-16T23:03:59.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/qoretechnologies/qlip","commit_stats":null,"previous_names":["qoretechnologies/vrt-cli","qoretechnologies/qlip"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/qoretechnologies/qlip","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qoretechnologies%2Fqlip","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qoretechnologies%2Fqlip/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qoretechnologies%2Fqlip/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qoretechnologies%2Fqlip/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/qoretechnologies","download_url":"https://codeload.github.com/qoretechnologies/qlip/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qoretechnologies%2Fqlip/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34647750,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-22T02:00:06.391Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-22T12:01:41.641Z","updated_at":"2026-06-22T12:01:42.515Z","avatar_url":"https://github.com/qoretechnologies.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Qlip\n\nQlip is a Storybook screenshot capture tool. Add it to your project and every story automatically gets a screenshot after its test finishes. Use `screenshot()` inside a play function to capture intermediate states.\n\n## Choosing your integration\n\nQlip ships **two integration paths**. They produce identical screenshot bundles and upload to the same server — pick by tooling preference, not project size.\n\n| Your situation | Path | Why |\n|---|---|---|\n| Greenfield, ≤150 stories | **Vitest plugin** (default config) | One extra dep. Screenshots happen as a side effect of `yarn test:stories`. |\n| 150+ stories / heavy components | **Vitest plugin (memory-safe config)** | Add `pool: 'forks' + maxForks: N` — see [Memory-safe config](#memory-safe-config-for-large-storybooks). |\n| Can't be on Vitest 4 / want zero-Vitest capture | **Runner** (`qlip-serve-and-test`) | Decoupled from consumer's Vitest version. |\n| Already running `@storybook/test-runner` | **Runner** | Add one `postVisit` hook to the test-runner you already have. |\n\nThe full comparison + when-to-switch guide is in [`design/INTEGRATION_PATHS.md`](./design/INTEGRATION_PATHS.md). The runner architecture is in [`design/RUNNER.md`](./design/RUNNER.md).\n\n## Install\n\n```bash\nnpm install --save-dev @qoretechnologies/qlip\n# Plus, only for the runner path:\nnpm install --save-dev @storybook/test-runner\n```\n\n## Vitest setup\n\n```ts\nimport { defineConfig } from 'vitest/config';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { storybookTest } from '@storybook/addon-vitest/vitest-plugin';\nimport { playwright } from '@vitest/browser-playwright';\nimport { qlipVitestPlugin } from '@qoretechnologies/qlip';\n\nconst dirname =\n  typeof __dirname !== 'undefined'\n    ? __dirname\n    : path.dirname(fileURLToPath(import.meta.url));\n\nexport default defineConfig({\n  test: {\n    projects: [\n      {\n        extends: true,\n        plugins: [\n          storybookTest({\n            configDir: path.join(dirname, '.storybook'),\n          }),\n          qlipVitestPlugin(),\n        ],\n        test: {\n          name: 'storybook',\n          browser: {\n            enabled: true,\n            headless: true,\n            provider: playwright({}),\n            instances: [{ browser: 'chromium' }],\n          },\n          setupFiles: ['.storybook/vitest.setup.ts'],\n        },\n      },\n    ],\n  },\n});\n```\n\n## Runner setup (for large Storybooks)\n\nUse this **instead of** the Vitest plugin if your suite is past the\nVitest-browser-mode memory envelope (see [Choosing your integration](#choosing-your-integration)\nabove).\n\n1. Add `.storybook/test-runner.ts`:\n\n```ts\nimport type { TestRunnerConfig } from '@storybook/test-runner';\nimport { getStoryContext } from '@storybook/test-runner';\nimport { qlipCapture } from '@qoretechnologies/qlip/test-runner';\n\nconst config: TestRunnerConfig = {\n  async postVisit(page, context) {\n    await qlipCapture(page, context, { getStoryContext });\n  },\n};\nexport default config;\n```\n\n2. Run it:\n\n```bash\n# Build storybook, serve it on a free port, capture every story\n# through @storybook/test-runner, upload as one build:\nyarn build-storybook\nQLIP_UPLOAD_URL=https://qlip.example.com \\\nQLIP_PROJECT=my-app \\\nyarn qlip-serve-and-test --shards 8 --continue-on-failure\n```\n\nFlags worth knowing:\n\n- `--shards \u003cn\u003e` — run N test-storybook invocations sequentially with `--shard k/N`. Each shard is a fresh subprocess; memory resets between shards. Default `1`.\n- `--continue-on-failure` — don't stop the loop on a non-zero shard exit. Right default for visual-regression demos where a failed play function doesn't invalidate the captured screenshot.\n- `--storybook-static \u003cdir\u003e` — point at an existing static Storybook dir (default `./storybook-static`).\n- `--url \u003curl\u003e` — skip the static server, capture against an already-running Storybook.\n- Anything after `--` is forwarded to `test-storybook` (e.g. `-- --maxWorkers 2`).\n\n`qlip-serve-and-test --help` for the full list.\n\n## Authenticating uploads\n\nWhen the qlip-server is started with `UPLOAD_TOKEN` set, every\nupload must carry `Authorization: Bearer \u003ctoken\u003e`. Both integration\npaths read this token from the **`QLIP_UPLOAD_TOKEN`** env var (the\nVitest plugin via `vitest.config.ts`, the runner CLIs via the same\nenv or an explicit `--token` flag). Against a localhost dev server\nwith `UPLOAD_TOKEN` unset, leave it blank — uploads are accepted\nwithout a header.\n\nThere are **two kinds of token** you can put in `QLIP_UPLOAD_TOKEN`,\nand qlip treats them identically — it just forwards the string as a\nbearer. The difference is server-side scope:\n\n| Token | Looks like | Scope | When to use |\n|-------|-----------|-------|-------------|\n| **Per-project** (recommended) | `qlt_…` (44 chars) | Exactly one project; uploads to any other project 403 | CI pipelines. Mint one per project from the dashboard's **Project settings → API tokens**; revoke it without disrupting other projects. |\n| **Super-admin** (`UPLOAD_TOKEN`) | whatever you set the env to | Every project, all scopes | Bootstrapping, or a single shared CI that uploads to many projects. The legacy/escape-hatch token; keep it secret. |\n\nPrecedence is simple because qlip only sends one token: whatever\nvalue is in `QLIP_UPLOAD_TOKEN` (or `--token`) is the one used. Pick\nthe **narrowest token that works** — a per-project `qlt_…` token for\na single-project CI, the super-admin token only when one uploader\ngenuinely spans projects. A per-project token must match the\n`QLIP_PROJECT` you're uploading to, or the server returns `403`.\n\n```bash\n# CI uploading to one project — scoped per-project token:\nQLIP_UPLOAD_URL=https://qlip.example.com \\\nQLIP_PROJECT=my-app \\\nQLIP_UPLOAD_TOKEN=qlt_xxxxxxxx… \\\nyarn test\n```\n\nSee `qlip-server`'s `design/API.md §0` (auth model) and `§18`\n(token CRUD) for the server side, and `design/UPLOAD.md` here for\nthe upload protocol.\n\n### CI checkout: use `fetch-depth: 0` for accurate baselines\n\nqlip auto-detects the commit ancestry (via `git rev-list`) and, on PR\nbuilds, the base branch (via `$GITHUB_BASE_REF`) and sends them with\nevery upload. The server walks the ancestry to find the nearest prior\nbuild to diff against. A **shallow** clone — `actions/checkout`'s\ndefault — only fetches the tip commit, so that walk has nothing to\nfollow and the server falls back to the project's default branch,\nproducing noisier diffs. Fetch full history:\n\n```yaml\n- uses: actions/checkout@v4\n  with:\n    fetch-depth: 0\n```\n\nqlip prints a one-time warning when it detects a shallow clone. It\nnever fails the upload over this — accuracy degrades, the run still\nsucceeds.\n\n## Manual screenshots inside play\n\nUse the existing **\"Logged In\"** story as a real-world example:\n\n```ts\nimport { screenshot } from '@qoretechnologies/qlip';\n\nexport const LoggedIn = {\n  play: async (ctx) =\u003e {\n    // interactions and assertions...\n    await screenshot(ctx, 'after-login');\n  },\n};\n```\n\n## Parameters\n\nConfigure screenshots per story via `parameters.qlip`:\n\n```ts\nexport const LoggedIn = {\n  parameters: {\n    qlip: {\n      skip: false,\n      viewport: { width: 1280, height: 720 },\n      disableAnimations: true,\n      pauseAnimationsAtEnd: false,\n      captureOnError: false,\n      waitForIdleMs: 300,\n      maxWaitForIdleMs: 2000,\n      ignoreElements: ['.toast', '[data-qlip-ignore]'],\n    },\n  },\n};\n```\n\nOptions precedence:\n\n1. Explicit options passed to `screenshot()`\n2. `parameters.qlip`\n3. Plugin defaults\n\n## Plugin options\n\n```ts\nqlipVitestPlugin({\n  outputDir: './qlip/screenshots',\n  viewport: { width: 1280, height: 720 },\n  disableAnimations: false,\n  pauseAnimationsAtEnd: false,\n  captureOnError: false,\n  waitForIdleMs: 300,\n  maxWaitForIdleMs: 2000,\n  ignoreElements: [],\n});\n```\n\n`waitForIdleMs` waits for DOM mutations to settle before taking a screenshot. This is especially useful for animation libraries like `react-spring` that update inline styles via `requestAnimationFrame`, which bypasses CSS-based animation disabling. Increase it if you still catch mid-transition frames, or lower it for faster runs when your UI is static. `maxWaitForIdleMs` caps the wait so stories with continuously changing UI still complete.\n\n`ignoreElements` lets you provide CSS selectors to mask before capture. Qlip draws solid overlays on matching elements so layout stays intact while visual diffs ignore those regions.\n\n## Memory-safe config for large Storybooks\n\nIf your Storybook has more than ~150 stories, or any stories that mount a heavy app shell (dashboards, multi-pane editors, etc.), the **default Vitest browser-mode** can OOM — it spawns one worker per CPU core, each with its own BrowserContext, and on a 12-core machine that's easily 20+ GB peak RAM.\n\nThe fix is **two extra lines** in your storybook project's test config — switch the pool to forks and cap concurrency:\n\n```ts\ntest: {\n  name: 'storybook',\n  browser: { enabled: true, headless: true, provider: playwright({}), instances: [{ browser: 'chromium' }] },\n  // ↓ The memory-safe knobs ↓\n  pool: 'forks',\n  poolOptions: {\n    forks: { maxForks: 3, minForks: 1, isolate: true },\n  },\n  setupFiles: ['.storybook/vitest.setup.ts'],\n}\n```\n\nEach fork peaks at ~3 GB RSS during a heavy story's chromium render, so the rule of thumb is `maxForks ≤ (free_RAM_GB - 4) / 3`:\n\n| Environment | Suggested `maxForks` |\n|---|---|\n| GitHub Actions Linux runner (~7 GB) | 1 |\n| 16 GB Mac (M1/M2 base) | 2 |\n| 24 GB Mac (M4 Pro / M3 Pro) | 3 |\n| 32 GB+ workstation | 4–6 |\n\nVerified on a real consumer (qorus-ide, 90 story files / 643 tests, Vitest 2.1.9, Storybook 8.5): **7 min wall time, 7.9 GB peak RSS, full suite completed with no OOM**. Full background in [`design/INTEGRATION_PATHS.md`](./design/INTEGRATION_PATHS.md#memory-safe-config-for-large-storybooks).\n\n## Output layout\n\n```\n./qlip/screenshots/\n  \u003cbuildId\u003e/\n    stories/\n      auto/\u003cstoryTitle\u003e--\u003cstoryName\u003e.png\n      manual/\u003cstoryTitle\u003e--\u003cstoryName\u003e--\u003cscreenshotName\u003e.png\n      error/\u003cstoryTitle\u003e--\u003cstoryName\u003e--qlip-auto-error-capture.png\n    manifest.json\n```\n\n- `buildId` defaults to `YYYYMMDD-HHmmss`\n- `parameters.qlip.skip === true` disables all captures for that story\n\n## Manifest\n\nEach run writes `manifest.json` inside the build folder with tool metadata, defaults, stats, and per-screenshot entries (auto + manual).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqoretechnologies%2Fqlip","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fqoretechnologies%2Fqlip","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqoretechnologies%2Fqlip/lists"}