09.03.07 — Refactor skills#

Skills that refactor the existing automated-test base.

Listing (potentially non-exhaustive)#

SkillTarget
refactor-fragmentationDRY based on user preference
introduce-pom-retriesPOM-internal retries, with split (first-try + with-retries)

refactor-fragmentation#

input  : the test base
output : DRY refactor suggestions:
            - blocks repeated in 3+ scenarios → extract into a fragment
            - near-identical connectors → extract a parameterized connector
            - repeated log+screenshot sequences → extract a helper
         + per-case recommendation

CLAUDE.md:

When to extract a fragment

  • Don’t extract preemptively. Two scenarios sharing 3 acts is a coincidence. Wait for 3+ scenarios with the same block.
  • The block must be a precondition/postcondition, not the focus of any test. valid_login.py does not use the login fragment — login is the test there, and uses log_and_screenshot for the meaningful page transition.
  • Unhappy-path login tests stay self-contained. failed_logins and unauthenticated_history_access don’t use the fragment — they must stay independent of demo-user state.
  1. Quantity: 3+ scenarios with the same block.
  2. Role: pre/postcondition, not the test’s focus.
  3. Independence: unhappy-path tests must stay self-contained.

refactor-fragmentation applies these criteria and suggests (without applying).

introduce-pom-retries#

input  : a connector that exercises a flaky action (e.g. clicking a button that may fail intermittently)
output : refactor into:
            1. POM method `<action>` without retry (single try)
            2. POM method `<action>_with_retries(retries: int, logger: ILogger)` that retries internally
            3. dual connectors:
                - `<action>`: calls POM `<action>`
                - `<action>_with_retries(retries, logger)`: calls POM `<action>_with_retries`
            4. scenarios updated to use the appropriate variant

That’s the pattern seen in ocarina-example/lib/connectors/test_steps/actions/dashboard_login.py:

def login_without_otp(creds: ImmutableCredentials):
    def unwrapped(p: DashboardLoginPage) -> DashboardLoginPage:
        return p.login_without_otp(creds)
    return unwrapped


def login_without_otp_and_with_retries(
    creds: ImmutableCredentials, retries: int, *, logger: ILogger
):
    def unwrapped(p: DashboardLoginPage) -> DashboardLoginPage:
        return p.login_without_otp_and_with_retries(creds, retries, logger=logger)
    return unwrapped

Two connectors. The scenario picks based on need:

  • Happy-path case: login_without_otp_and_with_retries (the 10%-fail useAuth forces the retry).
  • Unhappy-path case ("login with wrong password"): login_without_otp (we want the failure to surface, no retry).

Why split#

Without splittingWith splitting
One login(...) method, always with retryTests that want to see an immediate failure are clouded by retries
No happy / unhappy differentiationClear differentiation
Brittle tests if you remove the retryRobust tests: test coverage preserved; a complementary test refusing flakiness keeps tracing it on the side

Retries at the POM, not in the scenario#

Holy Book (chapter “First real-world hurdles”):

Test step hazards#

Thinking I had left that kind of nonsense behind me, I moved on, only to find unstable forms and authentication systems that worked half the time.

Here, Ocarina’s answer is different: we delegate the responsibility to the POM.

VariantProCon
Framework-level retry (transient_errors)Transparent, appropriate for flakiness that’s truly impossible to isolateReplays the whole test, not just the flaky action
POM-level retryGranular + placed in a well-named method — 

Cross-cutting discipline#

  • Analyze patterns in the base.
  • Suggest refactors.
  • Don’t immediately apply unless explicitly instructed.
  • Respect extraction rules.