skip to content

questions

4

Why does a system permission prompt need grant, deny and permanent-deny as three separate cases?

level: middleimportance: must knowfreq 60%

answer

  1. Three answers, three product behaviours
  2. One denial is really two states
  3. The suppressed prompt never arrives
  4. Nothing may wait on a silent answer
  5. Each case sets its own starting state

basics

~20 s

Three answers produce three different application behaviours: the feature works, the feature degrades and may ask again, and the feature must degrade with no prompt ever appearing. Only the third exposes the silent dead ends.

solid answer

~50 s

Each answer leaves the application in a different state, so each deserves its own case. **Grant** proves the feature works and that the capability is requested at the right moment. **Deny** proves the reduced path renders and that the application is still allowed to ask on the next deliberate attempt. **Permanent deny** — the stored state where the platform stops surfacing the request at all — proves the reduced path renders *and* that nothing waits on an answer that will never arrive. That third branch is where spinners that never stop, blank panels and dead "enable" buttons live, because code written as "ask, then continue when the answer comes back" simply never continues. Since these are device-level stored decisions rather than in-application choices, every case must establish its own starting state instead of inheriting whatever the previous case left behind.

code

pseudocode · 20 lines
pseudocode
for outcome in [GRANTED, DENIED_ASKABLE, DENIED_SUPPRESSED]:
  case "capture screen with camera " + outcome:
    arrange:
      clear_stored_decision(capability = "camera")
      set_stored_decision(capability = "camera", value = outcome)
      launch_app()

    act:
      open_capture_screen()

    assert:
      if outcome == GRANTED:
        expect_visible(live_preview)
      else:
        expect_visible(manual_entry_fallback)
        expect_settled_within(3 seconds)      # nothing awaiting an answer
        expect_not_visible(progress_indicator)

    cleanup:
      clear_stored_decision(capability = "camera")

go deeper

for a junior

Be ready to name the answers a person can give a capability prompt and to say that the application must behave differently for each. Recalling that a denial is a stored decision, not a one-off event, is what matters at this level.

for a middle

Explain why permanent denial is a separate case: the prompt stops appearing, so anything waiting on an answer waits forever. Be able to describe how a case establishes its starting permission state rather than inheriting it from the case before.

for a senior

Demonstrate production judgement — which assertions belong in each branch, why the prompt's own wording is never asserted, and how the three cases stay independent of run order on a shared pool of devices.

for a principal

Own the policy: how much of the capability matrix runs on every commit versus nightly, and whether a feature should be capability-gated at all when its degraded path is not worth shipping.

## A permission is a branch in your product, not a device quirk A capability-gated feature — one that cannot work without the camera, precise position, the contact list or the ability to post notifications — waits on an answer your code does not choose. That answer is stored by the operating system, outside the application, and it outlives restarts, sign-outs and new builds. Your application reads the stored answer and behaves differently depending on what it finds. Different behaviour on different inputs is a branch, and branches are what cases cover. Most teams write one case here. It starts from a device with no decision stored, drives the screen that triggers the request, answers affirmatively, and asserts the feature works. That case is worth having. It is also the branch that was never going to ship broken, because it is the one the developer exercised by hand while building the screen. ## Three stored states, three cases These are not two states with a variation. They are three, and the third behaves unlike the other two. | Stored state | What the person meets on the next attempt | What the case proves | |---|---|---| | **Granted** | The working feature | The request happens at the right moment and the capability is genuinely used | | **Denied, still askable** | A reduced feature, and the prompt again on a deliberate retry | The reduced path renders and the application may ask once more | | **Denied, no longer askable** | A reduced feature and *no prompt at all* | The reduced path renders with nothing waiting on an answer that never arrives | The third row is the whole reason for splitting. Once the platform stops surfacing the request, code shaped as "ask, then continue when the answer arrives" never continues. There is no answer, no completion, no error — only silence — and everything downstream in that flow is unreachable. ## What only the third case finds The defects that hide behind the no-longer-askable state are consistently the ugly ones: - a progress indicator that spins until the person force-quits, because the continuation is still waiting; - a panel with no content and no controls, because the fallback was wired only to the plain-denial path; - an inviting "enable" button that does nothing when tapped, since the request is issued, silently dropped, and never completes; - a crash on an empty capability handle, because the code assumed a request always yields either approval or an explicit refusal; - a loop that re-requests on every screen entry, which the platform absorbs and the person experiences as a broken control. None of these appear in the granted case, and only one of them shows up in the plain-denial case. ## Making the starting state part of the case Permission decisions are device state, not application state. A fresh installation may clear them; relaunching the application does not. Two consequences follow. 1. **Each case establishes its own starting state.** In its arrange step, before launch, it clears the stored decision for that capability and sets the one it needs. A case that only works because the case before it denied something is not a case; it is an ordering dependency wearing a case's name. 2. **Each case restores that state, or the run starts from a fresh installation.** On a shared pool of devices, a case that walks away leaving a capability permanently denied will quietly break every later case that assumed a clean device. Where the harness cannot write the stored decision directly, the arrange step drives the prompt to the intended answer explicitly, before any assertion begins — setup, never an incidental tap in the middle of a check. ## What these cases assert, and what they never assert Assert your product: which of your screens renders, what your copy says, whether the alternative route can be completed, and — in the third case — that nothing hangs. Never assert the prompt itself. Its wording is written by the platform, translated per language and changed between releases, so a case matching its headline fails on the next platform update for no product reason at all. Name each case after its branch while you are at it. "Capture screen, capability permanently denied, offers manual entry" tells someone reading a red run exactly which path broke; "permission test 3" tells them to open the code. ## The partial grant is a fourth state Some platforms let a person approve a weaker form of the capability — an approximate area rather than an exact position, a chosen handful of items rather than the whole collection. Where the product branches on that, it earns a fourth case, because its expected behaviour matches neither the granted path nor the degraded one: the feature works, at lower fidelity, and usually owes the person an explanation of why the result is coarse.

  • How do you stop the permanently-denied case from depending on the order the cases run in?
    Make the starting state part of the case rather than of the run. The case clears the stored decision for that capability and sets the value it needs before launching, so it produces the same result run alone, run first or run last. Where the harness cannot write that decision, install the build fresh and drive the prompt to the intended answer as an explicit arrange step. Never rely on an earlier case having denied it.
  • Is there a fourth case when the person approves a reduced form of the capability?
    Yes, and it is usually the least tested. Where the platform offers an approximate position instead of an exact one, or a selected subset instead of a whole collection, the product normally has to work coarsely rather than degrade. The assertion differs from both grant and deny: the feature renders, at lower fidelity, ideally with a visible explanation. Treat it as its own case whenever the code branches on it.
  • What should each of the three cases be named so a red run is readable?
    Put the branch in the name: the screen, the capability and the stored state, then the expected behaviour. Someone scanning failures should know which of the three paths broke without opening the code. Row numbers and sequence numbers push that work back onto the reader and make an intermittent failure much harder to triage.

A locked door has three states, not two: open, locked with a bell you may ring again, and locked with the bell disconnected. Only the third tells you whether the visitor knows to walk away instead of standing there.

saying these in an interview costs you the question

  • Treats denial and permanent denial as one case
  • Automates only the granted path and calls the feature covered
  • Asserts the exact wording of the system-drawn prompt
  • Assumes relaunching the application clears the stored decision
  • Claims the platform keeps prompting until the person agrees
  • Leaves the starting state to whatever the previous case did
open as a page

Why is a permission prompt drawn by the operating system not just another dialog in your application?

level: juniorimportance: should knowfreq 48%

basics

~20 s

The prompt belongs to the platform, not the product. Its wording and layout are decided outside the application, it may not appear at all when a decision is already stored, and it takes focus away from your own screens.

open as a page

When a case denies a capability permission, what should it assert beyond the application not crashing?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Assert the whole degraded journey: the fallback appears, explains what is unavailable, and can be completed to a real end state. Then assert what must not happen — no repeated prompting, no empty screen passed off as success, no dead controls.

open as a page

How do you test the path where a permission the application already holds is revoked mid-use?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

Take the permission away between two uses of the feature inside one case, then assert the second use lands on the degraded path rather than erroring or hanging. Products fail here by checking once at launch and caching the answer.

open as a page