{"id":15638753,"url":"https://github.com/oliyh/kamera","last_synced_at":"2025-06-22T11:05:26.292Z","repository":{"id":56211293,"uuid":"159835202","full_name":"oliyh/kamera","owner":"oliyh","description":"UI testing via image comparison and devcards","archived":false,"fork":false,"pushed_at":"2022-04-14T10:47:57.000Z","size":1174,"stargazers_count":93,"open_issues_count":10,"forks_count":7,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-02T17:08:14.800Z","etag":null,"topics":["clojure","clojurescript","devcards","image-comparison","testing"],"latest_commit_sha":null,"homepage":null,"language":"Clojure","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/oliyh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":["oliyh"]}},"created_at":"2018-11-30T14:30:52.000Z","updated_at":"2025-03-16T16:16:25.000Z","dependencies_parsed_at":"2022-08-15T14:41:08.292Z","dependency_job_id":null,"html_url":"https://github.com/oliyh/kamera","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oliyh%2Fkamera","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oliyh%2Fkamera/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oliyh%2Fkamera/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oliyh%2Fkamera/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/oliyh","download_url":"https://codeload.github.com/oliyh/kamera/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248095286,"owners_count":21046823,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["clojure","clojurescript","devcards","image-comparison","testing"],"created_at":"2024-10-03T11:22:59.989Z","updated_at":"2025-04-09T19:21:43.829Z","avatar_url":"https://github.com/oliyh.png","language":"Clojure","funding_links":["https://github.com/sponsors/oliyh"],"categories":[],"sub_categories":[],"readme":"# kamera\n\nVisual testing tools for Clojure with [figwheel-main](https://github.com/bhauman/figwheel-main)\nand [devcards](https://github.com/bhauman/devcards) integration.\n\n[![Clojars Project](https://img.shields.io/clojars/v/kamera.svg)](https://clojars.org/kamera)\n\nGive kamera some reference images and your devcards build and get automatic screenshots and comparisons to your references of all your devcards.\nIf you don't use figwheel or devcards, kamera can accept [a list of urls](#core-api) for you to roll your own.\n\n![](doc/report-card.png?raw=true)\n\n- [Why?](#why)\n- [Prerequesites](#prerequesites)\n- [Usage](#Usage)\n  - [Figwheel + devcards](#figwheel--devcards)\n  - [Core API](#core-api)\n- [Options](#options)\n- [Normalisation](#normalisation)\n- [Example use cases](#example-use-cases)\n\n## Why?\n\nWhen data is represented visually for a human to view you must take to present it intuitively, accessibly\nand beautifully. This requires skill, time and above all human judgement.\n\nOnce achieved you want to ensure it does not suffer regressions. kamera is a library designed to\nhelp you capture and compare screenshots of your application, failing if there is too much divergence between an expected\nreference image and the current display and creating a difference image to show you where.\n\nThe best way to test visual representation is to create [devcards](https://github.com/bhauman/devcards)\nwhich you can use to display components in as many states as possible. If you ensure you separate rendering from\nbusiness logic you can ensure that refactoring will not affect them and prevent them becoming brittle - I outlined this approach\nin a [blog post for JUXT](https://juxt.pro/blog/posts/cljs-apps.html).\n\n![](doc/magnifier.png?raw=true)\n\n## Prerequesites\n\nkamera uses ImageMagick for image processing and Chrome to capture screenshots.\nBy default it looks for them on the path, but you can supply paths if they reside somewhere else - see the [options](#options).\nkamera lets you choose the metric for image comparison - [read more about the choices here](https://imagemagick.org/script/command-line-options.php#metric).\n\n## Usage\n\n### figwheel + devcards\n\n_See the [example project](https://github.com/oliyh/kamera/tree/master/example) for a full working example._\n\nThe following assumes a figwheel-main build called `dev` which has devcards that you view at `/devcards.html`.\n\nIf you have the following devcards files:\n\n```bash\ntest\n└── example\n    ├── another_core_test.cljs\n    └── core_test.cljs\n```\n\n... and a directory populated with reference images, named in the same way:\n\n```\ntest-resources\n└── kamera\n    ├── example.another_core_test.png\n    └── example.core_test.png\n```\n\n_You can generate these images initially by running kamera and copying the 'actual' files from the target directory into your reference directory_\n\n... you can get kamera to screenshot the devcards and compare with the corresponding reference images with the following:\n\n```clojure\n(ns example.devcards-test\n  (:require [kamera.devcards :as kd]\n            [clojure.test :refer [deftest testing is]]))\n\n(deftest devcards-test\n  (kd/test-devcards \"dev\" kd/default-opts))\n```\n\nThe output will look like this:\n\n```clojure\nResults\n\nexample.kamera-test\n1 non-passing tests:\n\nFail in devcards-test\n#!/example.another_core_test\nexample.another_core_test.png has diverged from reference by 0.020624, please compare\nExpected: test-resources/kamera/example.another_core_test.png\nActual: target/kamera/example.another_core_test.png\nDifference: target/kamera/example.another_core_test-difference.png\nexpected: (\u003c metric metric-threshold)\n\n  actual: (not (\u003c 0.020624 0.01))\n```\n\nThe target directory will contain an expected, actual and difference image for every devcard.\nIt will also contain an html report which presents the juxtaposed images,\nthe normalisation steps and the difference compared to the threshold.\n\n![](doc/summary.png?raw=true)\n\n![](doc/report-card.png?raw=true)\n\n![](doc/magnifier.png?raw=true)\n\n### Core API\n\nIf you don't use figwheel or devcards you can still use kamera to take screenshots and compare them to reference images.\n\nYou will have to provide a list of \"targets\" for kamera to test.\nEach target must provide a `:url` and `:reference-file` and can override any setting from the `:default-target` [options](#options).\n\n```clojure\n(require '[kamera.core :as k])\n\n(k/run-tests [{:url \"http://localhost:9500/\"\n               :reference-file \"home.png\"}\n\n              {:url \"http://localhost:9500/preferences\"\n               :reference-file \"preferences.png\"\n               :metric-threshold 0.2}\n\n              {:url \"http://localhost:9500/help\"\n               :reference-file \"help.png\"\n               :metric \"RMSE\"\n               :normalisations [:trim]}])\n```\n\n## Options\n\n```clojure\n{:default-target                                   ;; default options for each image comparison\n   {:metric \"mae\"                                  ;; the imagemagick metric to use for comparison\n                                                   ;; see https://imagemagick.org/script/command-line-options.php#metric\n\n    :metric-threshold 0.01                         ;; difference metric above which comparison fails\n    :reference-directory \"test-resources/kamera\"   ;; directory where reference images are store\n    :screenshot-directory \"target/kamera\"          ;; directory where screenshots and diffs should be saved\n    :ready? (fn [session] ... )                    ;; predicate that should return true when screenshot can be taken\n                                                   ;; see element-exists? as an example\n    :normalisations [:trim :crop]                  ;; normalisations to apply to images before comparison, in order of application\n    :assert? true                                  ;; runs a clojure.test assert on the expected/actual when true, makes no assertions when false\n    :resize-to-contents {:height? true             ;; resize browser window dimensions to fit contents before screenshot - true for both is legacy behaviour\n                         :width? false\n                         :dom-selector \"body\"}     ;; which dom element dimensions are used for the resize\n\n :normalisation-fns                                ;; normalisation functions, add your own if desired\n   {:trim trim-images\n    :crop crop-images}\n\n :imagemagick-options\n   {:path nil                                      ;; directory where binaries reside on linux, or executable on windows\n    :timeout 2000}                                 ;; kill imagemagick calls that exceed this time, in ms\n\n :chrome-options                                   ;; options passed to chrome, letting you turn headless on/off etc\n                                                   ;; see https://github.com/tatut/clj-chrome-devtools/blob/master/src/clj_chrome_devtools/automation/launcher.clj#L52\n   {:chrome-binary \"/opt/bin/google-chrome-stable\"\n    :headless? true\n    :extra-chrome-args [\"--window-size=1600,900\"\n                        \"--hide-scrollbars\"]}\n}\n```\n\n### devcards options\n\nA few additional options exist if you are using the `kamera.devcards` namespace:\n\n```clojure\n{:devcards-options\n  {:path \"devcards.html\"            ;; the relative path to the page where the devcards are hosted\n   :init-hook (fn [session] ... )   ;; function run before attempting to scrape targets\n   :on-targets (fn [targets] ... )} ;; function called to allow changing the targets before the test is run\n}\n```\n\n## Normalisation\n\nWhen comparing images ImageMagick requires both input images to be the same dimensions.\nThey can easily differ when changes are made to your application, across operating systems or browser versions.\nNormalisation is the process of resizing both the expected and the actual images in a way that keeps the images lined up with one another\nfor the best comparison.\n\nThe built-in normalisations are `trim` and `crop`.\nThe former cuts out whitespace around image content and the latter crops the image canvas.\nThey are run sequentially and each stage is output to the target directory, giving a set of images as follows:\n\n```bash\nkamera\n├── example.core_test.actual.png\n├── example.core_test.actual.trimmed.cropped.png\n├── example.core_test.actual.trimmed.png\n├── example.core_test.expected.difference.png\n├── example.core_test.expected.png\n├── example.core_test.expected.trimmed.cropped.png\n└── example.core_test.expected.trimmed.png\n```\n\nYou can override the normalisations to each image, perhaps adding a `resize`:\n\n```clojure\n{:normalisations [:trim :resize :crop]}\n```\n\nAnd provide the `resize` function in the options map:\n\n```clojure\n{:normalisation-fns {:trim   trim-fn\n                     :crop   crop-fn\n                     :resize resize-fn}}\n```\n\nThe signature of `resize` should look like this:\n\n```clojure\n(defn resize-images [^File expected ^File actual opts])\n```\n\nAnd it should return `[expected actual]`. See the existing `trim` and `crop` functions for inspiration.\n\n## Example use cases\n\nHere are some example use cases you may wish to consider in addition to the standard ones given above:\n\n### Desktop / tablet / mobile testing\n\n```clojure\n(ns example.devcards-test\n  (:require [kamera.devcards :as kd]\n            [clojure.test :refer [deftest testing is]]))\n\n(deftest desktop-test\n  (kd/test-devcards\n   \"dev\"\n   (-\u003e kd/default-opts\n       (update :default-target merge\n               {:reference-directory \"test-resources/kamera/desktop\"}))))\n\n(deftest tablet-test\n  (kd/test-devcards\n   \"dev\"\n   (-\u003e kd/default-opts\n       (update :default-target merge\n               {:reference-directory \"test-resources/kamera/tablet\"})\n       (assoc-in [:chrome-options :chrome-args] [\"--headless\" \"--window-size=1024,768\"]))))\n\n(deftest mobile-test\n  (kd/test-devcards\n   \"dev\"\n   (-\u003e kd/default-opts\n       (update :default-target merge\n               {:reference-directory \"test-resources/kamera/mobile\"})\n       (assoc-in [:chrome-options :chrome-args] [\"--headless\" \"--window-size=800,600\"]))))\n```\n\n### Spot comparison during a webdriver test\n\n```clojure\n(ns example.devcards-test\n  (:require [kamera.core :as k]\n            [clojure.test :refer [deftest testing is]]))\n\n(deftest my-user-acceptance-test\n  (let [driver (init-driver {:host \"localhost\" :port 9500})]\n\n    ;;;  webdriver stuff happens ...\n\n    (navigate! driver \"/\")\n\n    (k/run-test {:url (.getUrl driver)\n                 :reference-file \"homepage-with-cookies-banner.png\"}\n                k/default-opts)\n\n    (click! driver \"#accept-cookies\")\n\n    (k/run-test {:url (.getUrl driver)\n                 :reference-file \"homepage-cookies-accepted.png\"}\n                k/default-opts)\n\n    (.quit driver)))\n\n```\n\n## Development\n\nStart a normal clj\u0026cljs repl.\n\nYou will need sassc for building the sass via `lein sass auto`.\n\ncljs tests: http://localhost:9500/figwheel-extra-main/auto-testing\ndevcards: http://localhost:9500/cards.html\n\n[![CircleCI](https://circleci.com/gh/oliyh/kamera.svg?style=svg)](https://circleci.com/gh/oliyh/kamera)\n\n## License\n\nCopyright © 2018 oliyh\n\nDistributed under the Eclipse Public License either version 1.0 or (at\nyour option) any later version.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foliyh%2Fkamera","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foliyh%2Fkamera","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foliyh%2Fkamera/lists"}