{"id":20143893,"url":"https://github.com/dominodatalab/cucu","last_synced_at":"2026-04-02T15:51:02.159Z","repository":{"id":257681389,"uuid":"458338179","full_name":"dominodatalab/cucu","owner":"dominodatalab","description":"cucu - Easy BDD web testing for local and CI, batteries included, extensible, and with descriptive reports. ","archived":false,"fork":false,"pushed_at":"2025-04-07T09:46:12.000Z","size":12468,"stargazers_count":7,"open_issues_count":4,"forks_count":4,"subscribers_count":9,"default_branch":"main","last_synced_at":"2025-04-07T10:32:58.654Z","etag":null,"topics":["bdd-style-testing-framework","end-to-end-testing"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/cucu/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause-clear","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dominodatalab.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-02-11T20:58:02.000Z","updated_at":"2025-03-06T17:48:27.000Z","dependencies_parsed_at":"2024-09-18T04:06:01.117Z","dependency_job_id":"cf32ec13-88a1-4ac2-ba4f-7b14df8dc232","html_url":"https://github.com/dominodatalab/cucu","commit_stats":null,"previous_names":["dominodatalab/cucu"],"tags_count":220,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dominodatalab%2Fcucu","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dominodatalab%2Fcucu/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dominodatalab%2Fcucu/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dominodatalab%2Fcucu/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dominodatalab","download_url":"https://codeload.github.com/dominodatalab/cucu/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247639931,"owners_count":20971548,"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":["bdd-style-testing-framework","end-to-end-testing"],"created_at":"2024-11-13T22:07:23.355Z","updated_at":"2026-04-02T15:51:02.145Z","avatar_url":"https://github.com/dominodatalab.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![pypi](https://img.shields.io/pypi/v/cucu.svg)](https://pypi.org/project/cucu/)\n[![license](https://img.shields.io/pypi/l/cucu.svg)](https://spdx.org/licenses/BSD-3-Clause-Clear.html)\n[![CI](https://dl.circleci.com/status-badge/img/gh/dominodatalab/cucu/tree/main.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/dominodatalab/cucu/tree/main)\n[![coverage](https://img.shields.io/endpoint?url=https%3A%2F%2Fdominodatalab.github.io%2Fcucu%2Fcoverage%2Fbadge%2Fbadge.json)](https://dominodatalab.github.io/cucu/coverage/badge)\n\n\n# ![Cucu Logo](https://raw.githubusercontent.com/dominodatalab/cucu/refs/heads/main/logo.png) **CUCU** - Easy BDD web testing \u003c!-- omit from toc --\u003e\n\nEnd-to-end testing framework that uses [gherkin](https://cucumber.io/docs/gherkin/)\nto drive various underlying tools/frameworks to create real world testing scenarios.\n\n\n## Why cucu? \u003c!-- omit from toc --\u003e\n1. Cucu avoids unnecessary abstractions (i.e. no Page Objects!) while keeping scenarios readable.\n    ```gherkin\n    Feature: My First Cucu Test\n      We want to be sure the user get search results using the landing page\n\n      Scenario: User can get search results\n        Given I open a browser at the url \"https://www.google.com/search\"\n         When I wait to write \"google\" into the input \"Search\"\n          And I click the button \"Google Search\"\n         Then I wait to see the text \"results\"\n    ```\n2. Designed to be run **locally** and in **CI**\n3. Runs a selenium container for you OR you can bring your own browser / container\n4. Does fuzzy matching to approximate actions of a real user\n5. Provides many steps out of the box\n6. Makes it easy to create **customized** steps\n7. Enables hierarchical configuration and env var and **CLI arg overrides**\n8. Comes with a linter that is **customizable**\n\n## Supporting docs \u003c!-- omit from toc --\u003e\n1. [CHANGELOG.md](CHANGELOG.md) - for latest news\n2. [CONTRIBUTING.md](CONTRIBUTING.md) - how we develop and test the library\n3. [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)\n4. [CONTRIBUTORS.md](CONTRIBUTORS.md)\n5. [LICENSE](LICENSE)\n\n# Table of Contents \u003c!-- omit from toc --\u003e\n\n- [Installation](#installation)\n  - [Requirements](#requirements)\n  - [Setup](#setup)\n  - [Usage](#usage)\n- [Usage](#usage-1)\n  - [Cucu Run](#cucu-run)\n  - [Run specific browser version with docker](#run-specific-browser-version-with-docker)\n- [Extending Cucu](#extending-cucu)\n  - [Fuzzy matching](#fuzzy-matching)\n    - [Relevance Scoring (Ordering)](#relevance-scoring-ordering)\n  - [Custom steps](#custom-steps)\n    - [Passing exceptions through](#passing-exceptions-through)\n  - [Before / After hooks](#before--after-hooks)\n  - [Custom lint rules](#custom-lint-rules)\n- [More Ways To Install Cucu](#more-ways-to-install-cucu)\n  - [Install From Build](#install-from-build)\n\n# Installation\nLet's get your repo to start using the cucu framework!\n\u003e [!NOTE] If you're not using uv, then just\n\u003e `pip install cucu` for your repo\n\n## Requirements\n1. Docker (for UI testing)\n2. The [uv](https://docs.astral.sh/uv/) tool\n3. A repo in a clean state\n\n## Setup\n\u003e [!NOTE] Always run cucu from your repo root folder\n\n1. Have docker running (don't need it right now, but why not get ready for testing)\n2. Make sure you have no files to commit and a clean working tree\n   ```\n   git status\n   ```\n   Should report: `nothing to commit, working tree clean`\n\n3. Add [cucu](https://pypi.org/project/cucu/) to your project and activate your venv\n   ```\n   uv add cucu --dev\n   source .venv/bin/activate\n   ```\n4. Initialize cucu (copies the `init_data` folder to your repo)\n   ```\n   cucu init\n   ```\n5. Manually resolve any file conflicts\n   ```\n   git status\n   ```\n6. Run the example tests\n   ```\n   cucu run features\n   ```\n7. Done! But there is more optional stuff you can do, like:\n   1. Run with `--no-headless` mode to see the browser interaction\n   2. Run with `--generate-report` (or `-g`) to generate the html **report/** folder\n   3. Reference the exact test (i.e. scenario) with `features/example.feature:8` instead of using the `features` folder\n   ```\n   cucu run features/example.feature:8 -g --no-headless\n   ```\n\n## Usage\n\u003e [!Note] Cucu needs to be run from your **repo root** (i.e. the parent of the `features` folder)\n\n\n1. list available cucu steps\n   ```bash\n   cucu steps\n   ```\n   - if you have `brew install fzf` then you can fuzzy find steps\n     ```bash\n     cucu steps | fzf\n     # start typing for search\n     ```\n2. **create your first cucu test**\n   - features/my_first_test.feature\n     ```gherkin\n     Feature: My First Cucu Test\n       We want to be sure the user get search results using the landing page\n\n       Scenario: User can get search results\n         Given I open a browser at the url \"https://www.google.com/search\"\n          When I wait to write \"google\" into the input \"Search\"\n           And I click the button \"Google Search\"\n          Then I wait to see the text \"results\"\n     ```\n3. **run it**\n   ```bash\n   cucu run features/my_first_test.feature\n   ```\n\n# Usage\n## Cucu Run\nThe command `cucu run` is used to run a given test or set of tests and in its\nsimplest form you can use it like so:\n```bash\ncucu run features/my_first_test.feature\n```\n\nThat would simply run the \"google search for the word google\" and once it's\nfinished executing you can use the `cucu report` command to generate an easy\nto navigate and read HTML test report which includes the steps and screenshots\nfrom that previous test run.\n\n*NOTE:*\nBy default we'll simply use the `Google Chrome` you have installed and there's\na python package that'll handle downloading chromedriver that matches your\nspecific local Google Chrome version.\n\nTo run Chrome with a specific profile (e.g. one that has extensions or login state), use `--chrome-profile-dir` with the **whole** Profile Path from `chrome://version` in Chrome (e.g. `/Users/my.user/Library/Application Support/Google/Chrome/Profile 1`). You can also set the env var `CUCU_CHROME_PROFILE_DIR`.\n\nExample:\n```bash\ncucu run features --chrome-profile-dir=\"/Users/my.user/Library/Application Support/Google/Chrome/Profile 1\" --no-headless\n```\n\n## Run specific browser version with docker\n\n[docker hub](https://hub.docker.com/) has easy to use docker containers for\nrunning specific versions of chrome, edge and firefox browsers for testing that\nyou can spin up manually in standalone mode like so:\n\n```bash\ndocker run -d -p 4444:4444 selenium/standalone-chrome:latest\n```\n\nIf you are using ARM64 CPU architecture (Mac M1 or M2), you must use seleniarm\ncontainer.\n\n```bash\ndocker run -d -p 4444:4444 seleniarm/standalone-chromium:latest\n```\n\nAnd can choose a specific version replacing the `latest` with any tag from\n[here](https://hub.docker.com/r/selenium/standalone-chrome/tags). You can find\nbrowser tags for `standalone-edge` and `standalone-firefox` the same way. Once\nyou run the command you will see with `docker  ps -a` that the container\nis running and listening on port `4444`:\n\nSpecific tags for seleniarm:\n[here](https://hub.docker.com/r/seleniarm/standalone-chromium/tags)\n\n```bash\n\u003e docker ps -a\nCONTAINER ID ... PORTS                                                NAMES\n7c719f4bee29 ... 0.0.0.0:4444-\u003e4444/tcp, :::4444-\u003e4444/tcp, 5900/tcp  wizardly_haslett\n```\n\n*NOTE:* For seleniarm containers, the available browsers are chromium and firefox.\nThe reason for this is because Google and Microsoft have not released binaries\nfor their respective browsers (Chrome and Edge).\n\nNow when running `cucu run some.feature` you can provide\n`--selenium-remote-url http://localhost:4444` and this way you'll run a very\nspecific version of chrome on any setup you run this on.\n\nYou can also create a docker hub setup with all 3 browser nodes connected using\nthe utilty script at `./bin/start_selenium_hub.sh` and you can point your tests\nat `http://localhost:4444` and then specify the `--browser` to be `chrome`,\n`firefox` or `edge` and use that specific browser for testing.\n\nThe docker hub setup for seleniarm: `./bin/start_seleniarm_hub.sh`\n*NOTE:* `edge` cannot be selected as a specific browser for testing\n\nTo ease using various custom settings you can also set most of the command line\noptions in a local `cucurc.yml` or in a more global place at `~/.cucurc.yml`\nthe same settings. For the remote url above you'd simply have the following\nin your `cucurc.yml`:\n\n```bash\nCUCU_SELENIUM_REMOTE_URL: http://localhost:4444\n```\n\nThen you can simply run `cucu run path/to/some.feature` and `cucu` would load\nthe local `cucurc.yml` or `~/.cucurc.yml` settings and use those.\n\n# Extending Cucu\n\n## Fuzzy matching\n\n`cucu` uses selenium to interact with the browser but on top of that we've\ndeveloped a fuzzy matching set of rules that allow the framework to find\nelements on the page by having a label and a type of element we're searching for.\n\nThe principal is simple you want to `click the button \"Foo\"` so we know you want\nto find a button which can be one of a few different kind of HTML elements:\n\n  * `\u003ca\u003e`\n  * `\u003cbutton\u003e`\n  * `\u003cinput type=\"button\"\u003e`\n  * `\u003c* role=\"button\"\u003e`\n  * etc\n\nWe also know that it has the name you provided labeling it and that can be\ndone using any of the following rules:\n\n  * `\u003cthing\u003ename\u003c/thing\u003e`\n  * `\u003c*\u003ename\u003c/*\u003e\u003cthing\u003e\u003c/thing\u003e`\n  * `\u003cthing attribute=\"name\"\u003e\u003c/thing\u003e`\n  * `\u003c*\u003ename\u003c/*\u003e...\u003cthing\u003e...`\n\nWhere `thing` is any of the previously identified element types. With the above\nrules we created a simple method method that uses the those rules to find a set\nof elements labeled with the name you provide and type of elements you're\nlooking for. We currently use [swizzle](https://github.com/jquery/sizzle) as\nthe underlying element query language as its highly portable and has a bit\nuseful features than basic CSS gives us.\n\n### Relevance Scoring (Ordering)\nWhen fuzzy matching yields multiple candidates, cucu orders results by a case-sensitive relevance score, then by discovery order as a tiebreaker. The `index` you pass to steps (e.g., the \"2nd\" button) refers to the Nth best-ranked element after sorting.\n\n- Priority by area (highest → lowest):\n   - Immediate text: direct TEXT_NODE children of the element.\n   - Attributes: selected attributes with sub-weights — `aria-label` \u003e `id` \u003e `class` (others like `title`, `placeholder`, `value` have no extra sub-weight).\n   - Full text: full element text including children.\n- Match strength within each area:\n   - Exact match outranks substring match.\n- Empty text fallback:\n   - If no area matches and the element has empty full text, a small default score is applied.\n- Tiebreaker:\n   - Earlier discovery pass (first found) wins ties with identical scores.\n\nNotes:\n- All matching is case-sensitive to mirror how selectors like `:contains` and custom `:has_text` behave.\n- The scoring is applied after deduplication by element identity.\n\n## Custom steps\nIt's easy to create custom steps, for example:\n1. create a new python file in your repo `features/steps/ui/weird_button_steps.py`\n    ```python\n    from cucu import fuzzy, retry, step\n\n    # make this step available for scenarios and listed in `cucu steps`\n    @step('I open the wierd menu item \"{menu_item}\"')\n    def open_jupyter_menu(ctx, menu_item):\n        # using fuzzy.find\n        dropdown_item = fuzzy.find(ctx.browser, menu_item, [\"li a\"])\n        dropdown_item.click()\n\n    # example using retry\n    def click_that_weird_button(ctx):\n        # using selenium's css_find_elements\n        ctx.browser.css_find_elements(\"button[custom_thing='painful-id']\")[0].click()\n\n    @step(\"I wait to click this button that isn't aria compliant on my page\")\n    def wait_to_click_that_weird_button(ctx):\n        # makes this retry with the default wait timeout\n        retry(click_that_weird_button)(ctx)  # remember to call the returned function `(ctx)` at the end\n    ```\n2. then update the magic `features/steps/__init__.py` file (this one file only!)\n\n   _Yeah I know that this is kind of odd, but work with me here😅_\n    ```python\n    # import all of the steps from cucu\n    from cucu.steps import *  # noqa: F403, F401\n\n    # import individual sub-modules here (i.e. module names of your custom step py files)\n    # Example: For file features/steps/ui/login.py\n    # import steps.ui.login_steps\n    import steps.ui.weird_button_steps\n    ```\n3. profit!\n\n### Passing exceptions through\n\nBy default, any exception raised in a step (other than `AssertionError`) is\nconverted to an `AssertionError` so failures are reported consistently. To let\nspecific exceptions pass through unchanged, use either of these:\n\n1. **Raise `CucuPassThroughError`** – In the step, raise\n   `CucuPassThroughError` to pass it through as-is, or\n   `raise CucuPassThroughError() from real_error` to unwrap so the real\n   exception type and traceback propagate:\n\n   ```python\n   from cucu import step, CucuPassThroughError\n\n   @step(\"I run something that may raise\")\n   def step_impl(ctx):\n       try:\n           some_library()\n       except ValueError as e:\n           raise CucuPassThroughError() from e\n   ```\n\n2. **Use the `exception_passthru` decorator parameter** – Declare which exception\n   type(s) this step may raise and should pass through (single class or tuple):\n\n   ```python\n   @step(\"I parse a number\", exception_passthru=ValueError)\n   def step_impl(ctx):\n       int(\"not a number\")  # ValueError passes through\n\n   @step(\"I do I/O\", exception_passthru=(OSError, RuntimeError))\n   def step_impl(ctx):\n       ...\n   ```\n\n## Before / After hooks\n\nThere are several hooks you can access, here's a few:\n```python\nregister_before_retry_hook,\nregister_before_scenario_hook,\nregister_custom_junit_failure_handler,\nregister_custom_tags_in_report_handling,\nregister_custom_scenario_subheader_in_report_handling,\nregister_custom_variable_handling,\nregister_page_check_hook,\n```\n\nAnd here's an example:\n1. add your function def to `features/environment.py`\n   ```python\n    import logging\n\n    from cucu import (\n        fuzzy,\n        logger,\n        register_page_check_hook,\n        retry,\n    )\n    from cucu.config import CONFIG\n    from cucu.environment import *\n\n    def print_elements(elements):\n        \"\"\"\n        given a list of selenium web elements we print their outerHTML\n        representation to the logs\n        \"\"\"\n        for element in elements:\n            logger.debug(f\"found element: {element.get_attribute('outerHTML')}\")\n\n    def wait_for_my_loading_indicators(browser):\n       # aria-label=\"loading\"\n       def should_not_see_aria_label_equals_loading():\n          # ignore the checks on the my-page page as there are these silly\n          # spinners that have aria-label=loading and probably shouldn't\n          if \"my-page\" not in browser.get_current_url():\n             elements = browser.css_find_elements(\"[aria-label='loading'\")\n             if elements:\n                   print_elements(elements)\n                   raise AssertionError(\"aria-label='loading', see above for details\")\n\n       retry(should_not_see_aria_label_equals_loading)()\n\n       # my-attr contains \"loading\"\n       def should_not_see_data_test_contains_loading():\n          elements = browser.css_find_elements(\"[my-attr*='loading'\")\n          if elements:\n             print_elements(elements)\n             raise AssertionError(\"my-attr*='loading', see above for details\")\n\n       retry(should_not_see_data_test_contains_loading)()\n\n       # class contains \"my-spinner\"\n       def should_not_see_class_contains_my_spinner():\n          elements = browser.css_find_elements(\"[class*='my-spinner'\")\n          if elements:\n             print_elements(elements)\n             raise AssertionError(\"class*='my-spinner', see above for details\")\n\n       retry(should_not_see_class_contains_my_spinner)()\n\n\n    register_page_check_hook(\"my loading indicators\", wait_for_my_loading_indicators)\n   ```\n2. done!\n\n## Custom lint rules\n\nYou can easily extend the `cucu lint` linting rules by setting the variable\n`CUCU_LINT_RULES_PATH` and pointing it to a directory in your features source\nthat has `.yaml` files that are structured like so:\n\n```yaml\n[unique_rule_identifier]:\n  message: [the message to provide the end user explaining the violation]\n  type: [warning|error] # I or W  will be printed when reporting the violation\n  current_line:\n    match: [regex]\n  previous_line:\n    match: [regex]\n  next_line:\n    match: [regex]\n  fix:\n    match: [regex]\n    replace: [regex]\n    -- or --\n    delete: true\n```\n\nThe `current_line`, `previous_line` and `next_line` sections are used to match\non a specific set of lines so that you can then \"fix\" the current line a way\nspecified by the `fix` block. When there is no `fix` block provided then\n`cucu lint` will notify the end user it can not fix the violation.\n\nIn the `fix` section one can choose to do `match` and `replace` or to simply\n`delete` the violating line.\n\n# More Ways To Install Cucu\n\n## Install From Build\n\nWithin the cucu directory you can run `uv build` and that will produce some\noutput like so:\n\n```bash\nBuilding source distribution...\nBuilding wheel from source distribution...\nSuccessfully built dist/cucu-0.207.0.tar.gz and dist/cucu-0.207.0-py3-none-any.whl\n```\n\nAt this point you can install the file `dist/cucu-0.1.0.tar.gz` using\n`pip install .../cucu/dist/cucu-*.tar.gz` anywhere you'd like and have the `cucu` tool ready to\nrun.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdominodatalab%2Fcucu","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdominodatalab%2Fcucu","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdominodatalab%2Fcucu/lists"}