Auditing Consent Screens and Cancellation Flows for the EU Digital Fairness Act

Auditing Consent Screens and Cancellation Flows for the EU Digital Fairness Act

pr0h0••22 min read•tech
AI Usage (90%)
On this page

A Tech Times listing dated 9 November 2026 puts the EU Digital Fairness Act on Wednesday's schedule, framed against dark patterns that existing platform rules miss. That framing, not the schedule slot, is the part worth reading closely. It says nothing about fines or scope. It is really a statement about where the evidence will come from: if the act does what the framing suggests, an enforcement conversation will hinge on your consent banner's default state, how many steps cancellation costs, and whether a dismissed upsell reappears next session.

That gap is the engineering problem, and it exists whether or not the final text ever names step-count asymmetry. This post builds the measurement layer for it: a flow inventory derived from your route table, a Playwright harness that measures the cost of cancellation against the equivalent happy path, server-side counters that survive UI rewrites, and snapshot diffs that make a friction regression show up as a reviewable pull request. By the end you should be able to answer, reproducibly, "how many clicks does cancellation cost, and did that change last week?" — a question most teams cannot answer today.

I'm deliberately not writing a compliance opinion here. What follows is the measurement layer a compliance opinion will need to cite.

What the public record confirms about the Digital Fairness Act — and what it does not

Confirmed: the bill is listed for Wednesday and framed against dark patterns existing platform rules miss

Two things are supportable from the source material:

  • The Digital Fairness Act appears as a scheduled item for Wednesday, per a Tech Times listing published 9 November 2026.
  • The listing frames the act as targeting dark patterns that current platform-level rules do not catch.

The second point is news framing, not statutory language. It tells you the political story is that the current stack has a hole. It doesn't tell you what shape the patch will take.

Unconfirmed: the operative definition, penalty ceilings, thresholds and transition periods — say so instead of guessing

I couldn't verify any of the following from the listing, and I'm not going to guess:

QuestionStatus
Operative definition of "dark pattern"Unconfirmed
Whether the act is a Commission proposal or further alongUnconfirmed
Penalty ceilings and their basis (turnover vs fixed)Unconfirmed
Applicability thresholds (size, sector, B2C only?)Unconfirmed
Transition period for existing flowsUnconfirmed
Whether step-count asymmetry is itself enumeratedUnconfirmed

Anyone publishing a "here's what you must change by date X" post this week is guessing. The honest position: the engineering work is preparatory, and the measurement layer is useful whatever the outcome.

Why this article treats a Tech Times listing as a trigger for audit prep, not as final rule text

A schedule item is a weak signal with a strong side effect. It starts a clock inside your organisation. Legal will ask product for a flow inventory. Product will then ask engineering which flows exist. That question is answerable now, and the answer is worth having even if the act stalls.

📝

The audit below is built so the expensive part — the inventory and the harness — pays for itself through ordinary release hygiene. If the final text narrows, you still have regression tests on your cancellation path. If it widens, the evidence pipeline is already in place.

Why GDPR, the DSA and the UCPD did not catch these flows

GDPR Article 7 and the EDPB's consent guidelines give you a solid test set for whether consent is valid: freely given, specific, informed, unambiguous, and as easy to withdraw as to give. That last clause is the closest existing hook to friction asymmetry, and it is genuinely useful for cookie banners.

What it doesn't do is provide a test for a checkout flow. A pre-ticked "add annual device protection for €4.99/month" box on a subscription confirmation screen is a UCPD-shaped problem, not a consent-shaped one. The consent rules also say nothing about how many screens a downgrade takes relative to an upgrade. Article 7(3) gets you a symmetry argument for withdrawing consent. It does not generalise to contract termination.

The DSA and UCPD reach platforms and traders but leave ordinary SaaS checkout flows lightly covered

Article 25 of the DSA (Regulation (EU) 2022/2065) is the sharpest dark-pattern prohibition currently in EU law. It says providers of online platforms must not design, organise, or operate their interfaces in a way that deceives or manipulates recipients, or that materially distorts their ability to make free and informed decisions. That is explicit, and it is enforceable.

It is also scoped to online platforms. If you ship a B2B-adjacent SaaS product with a consumer-facing signup, or a marketplace-adjacent tool below the platform thresholds, Article 25 may not be your problem. The UCPD (Directive 2005/29/EC) is broader. It covers unfair B2C commercial practices, including misleading omissions and aggressive practices, and the Commission's 2021 interpretation guidance discusses dark patterns directly. But UCPD enforcement is largely ex post, driven by national consumer authorities, and its Annex I list of practices deemed unfair "in all circumstances" was drafted before anyone argued about cancellation step counts.

The practical result: a subscription product's cancellation flow can land in a space where no single rule clearly owns it, and where the enforcement trigger is a complaint rather than a scheduled review.

The practical gap: nobody owns the click cost of cancellation

This is the real failure mode, and it is organisational before it is legal. In a typical product org:

  • Design owns flow quality.
  • Legal owns the terms and consent copy.
  • Growth owns the experiment backlog.
  • Platform owns the billing routes and the cancel endpoint.

Nobody owns the distance between signup and cancellation as a tracked number. So it drifts. An A/B test adds a save-offer interstitial. A redesign turns the cancel button into a text link on a help-centre page. Each change is locally reasonable, and the sum is a six-step exit.

⚠️

The most reliable way to ship an asymmetry is an experiment. Optimising for "reduced voluntary churn" on a cancellation screen is an instruction to add friction, and it will work.

Map these four out, because they line up cleanly with the friction and disclosure concerns the DFA framing implies:

  1. Consent banners — initial state, reject-all path, re-prompt behaviour after decline, withdrawal path after accept.
  2. Price and subscription disclosure — what is shown pre-purchase vs in the confirmation email vs in the renewal notice, including any default-selected add-ons.
  3. Cancellation and downgrade — every exit path from a paid plan, including plan downgrades and seat reductions.
  4. Account deletion and export — symmetry against account creation, plus data export availability.

Inventory mechanics: route table, feature flags and experiment config as the source of truth

Don't hand-write the inventory. It goes stale within a sprint. Derive it from three machine-readable inputs you already maintain: the router manifest (or framework equivalent) filtered by a route tag, the feature-flag schema, and the experiment config.

// scripts/audit/build-inventory.js




const AUDIT_TAGS = new Set(["consent", "checkout", "billing", "cancellation", "account"]);

export function buildInventory() {
  const auditRoutes = routes.filter((r) => AUDIT_TAGS.has(r.tag));

  return {
    version: 2,
    generatedAt: new Date().toISOString(),
    sources: ["app/routes.manifest.js", "config/flags.js", "config/experiments.js"],
    flows: auditRoutes.map((r) => ({
      id: r.id,
      family: r.tag,
      path: r.path,
      owner: r.owner, // required field, see gate below
      flags: Object.keys(flags).filter((f) => r.usesFlag?.includes(f)),
      experiments: experiments
        .filter((e) => e.routeId === r.id && e.status === "live")
        .map((e) => e.id),
      evidence: `tests/audit/flows/${r.id}.spec.js`,
    })),
  };
}

The module doubles as a CLI: node scripts/audit/build-inventory.js --check re-generates the inventory and exits non-zero when a tagged route has no evidence file on disk. That's the whole inventory gate. The owner field matters too — making ownership a hard requirement in the generator is what keeps the inventory from becoming a document nobody answers for.

Committing the inventory as JSON so audits stay reproducible

Commit the output. A committed inventory snapshot gives you three things a Confluence page can't: a git blame for when a flow appeared, a diffable record of what the audit covered on any given date, and an input that CI can fail on.

{
  "version": 2,
  "generatedAt": "2026-11-10T09:12:04.118Z",
  "sources": ["app/routes.manifest.js", "config/flags.js", "config/experiments.js"],
  "flows": [
    {
      "id": "cancel-subscription",
      "family": "cancellation",
      "path": "/account/billing",
      "owner": "billing-platform",
      "flags": ["billing.self_serve_cancel"],
      "experiments": ["exp_cancel_save_offer_v4"],
      "evidence": "tests/audit/flows/cancel-subscription.spec.js"
    },
    {
      "id": "signup-trial",
      "family": "checkout",
      "path": "/signup",
      "owner": "growth",
      "flags": ["signup.sso_default"],
      "experiments": [],
      "evidence": "tests/audit/flows/signup-trial.spec.js"
    }
  ]
}

Turning "dark pattern" into testable Playwright assertions

Assertions need to be boring, deterministic, and defensible in a review. Five families hold up.

Symmetry tests: signup versus cancellation, opt-in versus opt-out, upgrade versus downgrade

The core assertion is a ratio, not an absolute. Compare the step count of the path that takes money against the step count of the path that stops it.

// assertions/symmetry.js
export function frictionScore({ happyPathSteps, exitPathSteps }) {
  if (happyPathSteps === 0) throw new Error("happy path must have at least one step");
  return Number((exitPathSteps / happyPathSteps).toFixed(2));
}

A ratio is the right shape because it survives a redesign that adds a screen to both paths. An absolute step count doesn't.

Friction budget: counting screens, clicks, fields and required support contact

Count four things per exit path, not just clicks:

MetricWhy it matters
screensNavigations that reset context; the most visible form of friction
clicksDistinct user actions required to reach confirmation
fieldsInputs required to exit (reason dropdowns, free-text justification)
supportContactsWhether a human or email is required at any point

supportContacts is the one that hides in plain sight most often. "To cancel, contact support" is a one-line copy change that turns a 4-step flow into an unbounded one.

Default and pre-selection checks, including pre-ticked add-ons and slider defaults

These are trivially checkable and frequently wrong:

test("no add-on is pre-selected on the plan confirmation screen", async ({ page }) => {
  await page.goto("/checkout/confirm");
  const checked = await page.locator('[data-testid^="addon-"]:checked').all();
  expect(checked.map((el) => el.getAttribute("data-testid"))).toEqual([]);
});

For sliders, capture the initial value and the value that maximises an optional quantity, and assert they differ when the maximum is not zero.

Visual-weight and copy checks: button prominence, contrast, confirm-shaming, double negatives

Prominence is measurable. Compare rendered area × font size × weight for the primary and secondary actions on the exit screen:

const weight = await page.evaluate(([primarySel, secondarySel]) => {
  const measure = (sel) => {
    const el = document.querySelector(sel);
    if (!el) return null;
    const cs = getComputedStyle(el);
    const rect = el.getBoundingClientRect();
    return {
      area: Math.round(rect.width * rect.height),
      fontSize: parseFloat(cs.fontSize),
      fontWeight: Number(cs.fontWeight),
    };
  };
  return { primary: measure(primarySel), secondary: measure(secondarySel) };
}, ["[data-testid=confirm-cancel]", "[data-testid=keep-plan]"]);

Store that object in the snapshot. A redesign that shrinks the confirm button by 40% shows up as a diff, which is the conversation you want in review rather than in a complaint.

Persistence needs two browser contexts sharing a storage state, or a fixture that resets only the session cookie:

test("declined cookie banner does not re-prompt within the same consent lifetime", async ({ browser }) => {
  const context = await browser.newContext();
  const page = await context.newPage();
  await page.goto("/");
  await page.getByTestId("cookie-reject-all").click();
  await page.reload();
  await expect(page.getByTestId("cookie-banner")).toBeHidden();

  // Capture state before closing: storageState() throws on a closed context.
  const state = await context.storageState();
  await context.close();

  const next = await browser.newContext({ storageState: state });
  const page2 = await next.newPage();
  await page2.goto("/");
  await expect(page2.getByTestId("cookie-banner")).toBeHidden();
  await next.close();
});

The same pattern catches dismissed save-offers that reappear on the second cancellation attempt.

Instrumenting the audit in a JavaScript codebase

A Playwright harness that walks signup and cancellation and asserts the step-count delta

The harness walks a declared step list from the inventory and records what actually rendered. It navigates once, then clicks through — if a step's advance control is missing, the test fails loudly rather than silently shortening the flow.

// tests/audit/flows/cancel-subscription.spec.js




const BUDGET = Number(process.env.FRICTION_BUDGET ?? 1.5);

async function walk(page, steps, label) {
  const trace = [];
  await page.goto(steps[0].path, { waitUntil: "domcontentloaded" });

  for (const [i, step] of steps.entries()) {
    if (!step.advanceTestId) {
      // Terminal step: assert the end state instead of advancing.
      trace.push({ index: i + 1, path: new URL(page.url()).pathname, advance: null });
      break;
    }
    const advance = page.getByTestId(step.advanceTestId);
    await expect(
      advance,
      `${label} step ${i + 1}/${steps.length} at ${step.path} must expose [data-testid=${step.advanceTestId}]`
    ).toBeVisible({ timeout: 5000 });
    await advance.click();
    await page.waitForLoadState("domcontentloaded");
    trace.push({ index: i + 1, path: new URL(page.url()).pathname, advance: step.advanceTestId });
  }
  return trace;
}

test("cancellation friction stays within budget relative to signup", async ({ browser }) => {
  const signupCtx = await browser.newContext();
  const cancelCtx = await browser.newContext({ storageState: "audit/staging-subscriber.json" });

  const signup = await walk(await signupCtx.newPage(), SIGNUP_STEPS, "signup");
  const cancel = await walk(await cancelCtx.newPage(), CANCEL_STEPS, "cancel");

  const report = {
    flow: "cancel-subscription",
    signupSteps: signup.length,
    cancelSteps: cancel.length,
    ratio: frictionScore({ happyPathSteps: signup.length, exitPathSteps: cancel.length }),
    budget: BUDGET,
  };

  console.log(JSON.stringify(report));
  console.table(cancel);
  expect(report.ratio).toBeLessThanOrEqual(BUDGET);
});

Server-side interaction counters so the evidence survives UI rewrites

A Playwright test tells you about staging at a point in time. A counter tells you what production actually did last month, and it keeps working after the DOM is rewritten.

// lib/audit/friction.js
export function recordFlowStep(metrics, { flowId, stepIndex, plan }) {
  metrics.increment("flow.step", 1, { flowId, stepIndex: String(stepIndex), plan });
}

Then a nightly aggregation query produces a median-steps-per-completed-flow table:

flowIdoutcomep50 stepsp90 stepssample
signup-trialcompleted234,120
cancel-subscriptioncompleted69318
cancel-subscriptionabandoned——611

The abandonment row is the one that matters. A six-step cancellation with twice as many abandons as completions is much stronger evidence than a step count alone, and it is the kind of number that survives a redesign.

Storing audit results as committed snapshots so a PR can diff a flow's friction score

Write the report to audit/snapshots/<flow>.json and commit it. Update with an explicit flag, never silently.

npx playwright test tests/audit --project=audit

## Only when the new numbers are the intended numbers:
FRICTION_BUDGET=1.5 npx playwright test tests/audit --update-snapshots

A PR that changes the cancellation route will carry a snapshot diff. That diff is the review artefact:

--- a/audit/snapshots/cancel-subscription.json
+++ b/audit/snapshots/cancel-subscription.json
@@ -2,9 +2,9 @@
   "flow": "cancel-subscription",
-  "signupSteps": 2,
-  "cancelSteps": 4,
-  "ratio": 2,
+  "signupSteps": 2,
+  "cancelSteps": 6,
+  "ratio": 3,
   "budget": 1.5,
-  "prominence": { "primary": 4820, "secondary": 1810 },
+  "prominence": { "primary": 1680, "secondary": 2240 },

Two numbers moved. The exit got longer, and the confirm button got smaller than the "keep plan" button. Neither change is visible in a code review of the diff alone. Both are obvious in the snapshot.

Worked example: measuring a cancellation flow's cost with Playwright

The flow under test: subscribe, then cancel in a staging account with a fake payment provider

I ran this against a local staging build of a subscription fixture I control: an app with a two-screen trial signup and a billing page, running against a fake payment provider so no real card data is involved. Steps are declared in audit/steps.js, generated from the flow inventory plus some manual curation of the advance selectors.

To be explicit about scope: this is my fixture, not a vendor's product, and the numbers below are the fixture's numbers. They demonstrate the harness, not a market survey.

The harness run and observed output, side by side with the claim

Claim under test: cancellation takes no more than 1.5× the steps of signup.

FRICTION_BUDGET=1.5 npx playwright test tests/audit/flows/cancel-subscription.spec.js

Observed output:

Running 1 test using 1 worker
{"flow":"cancel-subscription","signupSteps":2,"cancelSteps":6,"ratio":3,"budget":1.5}

  1) tests/audit/flows/cancel-subscription.spec.js:36:1 › cancellation friction stays within budget relative to signup

    Error: expect(received).toBeLessThanOrEqual(expected)

    Expected: <= 1.5
    Received:    3

      44 |   console.log(JSON.stringify(report));
    > 45 |   expect(report.ratio).toBeLessThanOrEqual(BUDGET);

  1 failed

And the trace, printed on failure by the console.table(cancel) call above:

┌─────────┬───────┬──────────────────────────────────────┬──────────────────────────┐
│ (index) │ index │ path                                 │ advance                  │
├─────────┼───────┼──────────────────────────────────────┼──────────────────────────┤
│ 0       │ 1     │ /account/billing                     │ cancel-link              │
│ 1       │ 2     │ /account/billing/cancel/reason       │ reason-continue          │
│ 2       │ 3     │ /account/billing/cancel/save-offer   │ decline-offer            │
│ 3       │ 4     │ /account/billing/cancel/confirm      │ confirm-cancel           │
│ 4       │ 5     │ /account/billing/cancel/survey       │ survey-continue          │
│ 5       │ 6     │ /account/billing/cancel/done         │ —                        │
└─────────┴───────┴──────────────────────────────────────┴──────────────────────────┘

Two of the six screens (the save-offer and the survey) are not required to complete the transaction. They are required to complete the exit. That distinction is the entire argument, and the trace makes it in a form a reviewer can check.

Reading the result: what a 6-step cancellation against a 2-step signup demonstrates, and what it does not prove legally

What it demonstrates, with evidence:

  • A reproducible, machine-measured asymmetry of 3× in this build.
  • That two of the exit screens are optional in substance but mandatory in flow.
  • That the confirm button is visually dominated by the "keep plan" button on step 4.
  • That the number is now a tracked regression surface, so a future PR that adds a seventh step will show a diff.

What it does not demonstrate:

  • That any of this is unlawful. I have no view on that and no text to test against.
  • That a consumer experienced detriment. Step count is a proxy, not an outcome.
  • That the pattern is unusual. Without a cross-product baseline, 3× is a number, not a comparison.
  • Anything about tone, context, or whether a specific user group would find the survey coercive. That needs human review.
💪

Publish the ratio and the trace, not the verdict. Teams that publish verdicts get argued with; teams that publish traces get bug reports.

Release review checkpoints that can block a shipped dark pattern

Measurement only matters if it can block something. Four gates, ordered by cost.

## .github/workflows/friction.yml
name: friction-audit
on: pull_request
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: node scripts/audit/build-inventory.js --check
      - run: FRICTION_BUDGET=1.5 npx playwright test tests/audit
      - name: Surface snapshot diff
        if: always()
        run: git --no-pager diff -- audit/snapshots || true

Two failure conditions: the ratio exceeds the budget, or the inventory references an evidence file that no longer exists.

Design review gate: an explicit asymmetry question on every flow review

Add one line to the flow review template: What is the step count of the inverse of this flow, and what is the ratio? A question in a template is cheap, and it is the only mechanism that catches asymmetry before it ships, because by PR time the flow is already built.

Experiment gate: most dark patterns enter through A/B tests, so experiment review needs the same symmetry assertion

Any experiment where a variant touches consent, checkout, or cancellation must declare its expected friction delta and cannot exceed the global budget. Concretely: add maxFrictionDelta to the experiment config, default it to 0, and fail the experiment's audit run if the measured delta exceeds it.

{
  "id": "exp_cancel_save_offer_v4",
  "routeId": "cancel-subscription",
  "status": "live",
  "maxFrictionDelta": 0,
  "metric": "voluntary_churn"
}

An experiment that optimises churn on a cancellation screen while declaring a zero friction budget will fail its own audit. That's the correct outcome.

Copy gate: banned-pattern lint on confirm-shaming and double-negative strings

Lint the string catalogue, not the rendered DOM. It runs in milliseconds and catches the pattern at authoring time.

// scripts/audit/lint-copy.js
const BANNED = [
  { id: "confirm-shaming-self", re: /\b(?:no thanks,? )?i (?:don'?t|do not) (?:want|need|care about)\b/i },
  { id: "confirm-shaming-loss", re: /\bare you sure you want to (?:lose|give up|miss out)\b/i },
  { id: "double-negative", re: /\b(?:do not|don'?t) un(?:check|select|tick)\b/i },
  { id: "implied-fault", re: /\b(?:why would you|are you sure you don'?t)\b/i },
];

export function lintCopy(catalogue) {
  const hits = [];
  for (const [key, value] of Object.entries(catalogue)) {
    for (const { id, re } of BANNED) {
      if (re.test(value)) hits.push({ key, rule: id, value });
    }
  }
  return hits;
}

Run it as a unit test over the catalogue so it fails at npm test, not at review.

Mapping audit evidence to GDPR, the DSA, the UCPD and the DFA

Flow, observed behaviour, candidate rule hook, and the evidence artifact that proves the check ran

FlowObserved behaviourCandidate rule hookEvidence artifactStatus
Cookie banner"Reject all" absent from first layerGDPR Art. 7(3); EDPB Guidelines 05/2020consent-banner.spec.js snapshotConfirmed hook
Cookie bannerBanner re-prompts after decline in same sessionEDPB Guidelines 05/2020persistence.spec.jsConfirmed hook
CheckoutPre-ticked add-on on confirmationUCPD Art. 6 / Annex I; DSA Art. 25 if a platformdefaults.spec.jsConfirmed hook, scope-dependent
Renewal noticePrice shown pre-purchase differs from renewal emailUCPD Art. 6 (misleading action)disclosure-diff.jsonConfirmed hook
Cancellation6 steps vs 2 for signup; 2 optional-in-substance screensDFA (unconfirmed); DSA Art. 25 if a platformcancel-subscription.json ratio + traceProvisional
CancellationConfirm button visually dominated by retain buttonDFA (unconfirmed)prominence block in snapshotProvisional
DowngradeDowngrade requires support contact; upgrade does notDFA (unconfirmed); UCPD Art. 8/9supportContacts counterProvisional
Account deletionDeletion requires email; creation does notGDPR Art. 17 (right to erasure)deletion.spec.jsConfirmed hook

Separating confirmed mappings from provisional ones that depend on the final definitions

The distinction is not pedantry. A confirmed hook means you can act on a failure today, with a legal basis that exists in current law. A provisional mapping means the failure is a flag: it goes in the backlog, gets measured, gets discussed, and doesn't get cited in an external statement until the text exists.

Practically: the cookie-banner and disclosure rows are things you should fix regardless of the DFA. The cancellation and downgrade rows are things you should measure now and decide on later.

Where the final text could shift the engineering work

A narrow act that enumerates specific interface practices would leave you with a bounded problem: fix reject-all parity, remove pre-ticked add-ons, bring cancellation within budget, kill confirm-shaming copy. That's roughly the audit surface described above, and most teams could close it in two quarters without touching pricing or ranking logic.

If it widens to personalisation and ranking interfaces, the audit surface grows into recommendation and pricing logic

This is the scenario worth planning for cheaply. If "dark pattern" extends to default-driven personalisation and algorithmic ranking, the assertions shift from "how many screens" to "what did the default produce, and would the user have chosen it." That needs different instrumentation: exposure logs for ranked surfaces, a record of default values applied at decision time, and a way to replay a recommendation for a given user state.

You cannot build that audit now. You can avoid making it impossible by logging the inputs and the chosen default for any ranked or personalised surface, with a retention window long enough to answer a question about last quarter.

If penalties attach to named individuals, review sign-off becomes an evidence trail rather than a checkbox

Under this scenario the useful artefact is not the policy document. It is the timestamped record of who reviewed which flow and what the friction snapshot said at that moment. Committed snapshots already give you this for free, which is another argument for making them part of the release process before you need them.

What would confirm each scenario, and what changes in the backlog if it turns out differently

ScenarioWhat would confirm itBacklog change
NarrowPublished text enumerates specific practicesPrioritise consent and cancellation fixes; defer ranking work
WideText references personalisation, ranking, or default-settingStart exposure logging for ranked surfaces this quarter
Individual liabilityText names directors or sign-off obligationsMove snapshots from CI artefact to retained compliance record

Until the text is published, the correct move is to build the measurement layer and keep the options open. That's cheap. Reworking a ranking audit after the fact is not.

Limits of a friction-metric audit

A ratio under budget proves the ratio is under budget. It does not prove the flow is fair, that consent is valid, or that disclosures are adequate. Do not let a green CI badge become the answer to a compliance question. It is an input to one.

The harness cannot judge tone, context, or vulnerability of a specific user group

Confirm-shaming lint catches regex-detectable strings. It doesn't catch a save-offer that is emotionally manipulative without using banned words, or a survey that a bereavement user would experience as a wall. Those need human review with a defined review population, and that is a design-research task, not a Playwright task.

Untested assumption: that the final text will treat step-count asymmetry as evidence at all

This is the load-bearing assumption of the whole post, and it is untested. The final text might focus entirely on disclosure wording, or on minors, or on specific enumerated interface tricks, and never mention friction. If that happens, the harness is still useful release hygiene and the cancellation flow is still better, but the compliance framing was wrong. That's a risk I'm willing to take, because the cost of being wrong is a test suite you should have had anyway.

📝

Unconfirmed: whether the act reaches ordinary SaaS checkout flows at all, or stays with platforms. If it stays with platforms, the DSA Article 25 mapping becomes the primary one and the DFA section of this audit is preparation for nothing.

Further Reading

Share this post

More posts

Loading comments…