Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/handle-not-available-message.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'ePDS': patch
---

Handle-picker error wording is now accurate when the handle is reserved.

**Affects:** End users

**End users:** if you tried to claim a handle that was on the reserved list (admin, www, …) the picker said "That handle was just taken — please choose another." That's misleading — it wasn't just taken, it's permanently unavailable. The wording is now "That handle is not available — please choose another." which doesn't imply you can wait it out.
39 changes: 39 additions & 0 deletions e2e/step-definitions/handle-availability.steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Then, When } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import { testEnv } from '../support/env.js'
import { getPage } from '../support/utils.js'
import type { EpdsWorld } from '../support/world.js'

When(
'the handle picker preview reports that {string} is unavailable',
async function (this: EpdsWorld, handle: string) {
const page = getPage(this)
await page.route('**/api/check-handle?*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ available: false }),
})
})
await page.goto(`${testEnv.authUrl}/preview/choose-handle`)
await page.getByLabel('Handle').fill(handle)
},
)

Then(
'the handle picker shows {string}',
async function (this: EpdsWorld, expected: string) {
await expect(getPage(this).locator('#handle-status')).toHaveText(
`✗ ${expected}`,
)
},
)

Then(
'the handle picker does not show {string}',
async function (this: EpdsWorld, unexpected: string) {
await expect(getPage(this).locator('#handle-status')).not.toContainText(
unexpected,
)
},
)
12 changes: 12 additions & 0 deletions features/handle-availability.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Feature: Handle availability feedback
The picker must not claim that every unavailable handle is already taken,
because policy and validation failures can produce the same unavailable
response.

Background:
Given the ePDS test environment is running

Scenario: Generic unavailable response uses truthful copy
When the handle picker preview reports that "alreadyused" is unavailable
Then the handle picker shows "Not available."
And the handle picker does not show "Already taken."
16 changes: 8 additions & 8 deletions packages/auth-service/src/routes/choose-handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ export function createChooseHandleRouter(
}

const KNOWN_ERROR_MESSAGES: Record<string, string> = {
handle_taken: 'That handle was just taken — please choose another.',
handle_taken: 'That handle is not available — please choose another.',
}
const rawError = req.query.error as string | undefined
const error = rawError
Expand Down Expand Up @@ -380,7 +380,7 @@ export function createChooseHandleRouter(
.send(
renderChooseHandlePage(
handleDomain,
'That handle is already taken.',
'That handle is not available.',
res.locals.csrfToken,
showRandomButton,
branding.customCss,
Expand Down Expand Up @@ -530,7 +530,7 @@ export function renderChooseHandlePage(
}
.status { min-height: 20px; font-size: 14px; margin-top: 6px; }
.status.available { color: #28a745; }
.status.taken { color: #dc3545; }
.status.unavailable { color: #dc3545; }
.status.checking { color: #888; }
.status.format-error { color: #dc3545; }
.error { color: #dc3545; background: #fdf0f0; padding: 12px; border-radius: 8px; margin-bottom: 16px; font-size: 14px; }
Expand Down Expand Up @@ -587,8 +587,8 @@ export function renderChooseHandlePage(
var debounceTimer = null;
var currentAbort = null;

// isAvailable: null = unknown, true = confirmed available, false = confirmed taken.
// submitBtn is disabled only when handle is confirmed taken or unavailable.
// isAvailable: null = unknown, true = confirmed available, false = confirmed unavailable.
// submitBtn is disabled only when the handle is confirmed unavailable.
var isAvailable = null;

function setStatus(text, cls) {
Expand Down Expand Up @@ -626,14 +626,14 @@ export function renderChooseHandlePage(
setStatus('\u2713 Available!', 'available');
} else {
isAvailable = false;
setStatus('\u2717 Already taken.', 'taken');
setStatus('\u2717 Not available.', 'unavailable');
}
updateSubmit();
})
.catch(function(err) {
if (err.name === 'AbortError') return; // silently ignore cancelled requests
currentAbort = null;
// Network/timeout error: unknown state — don't block if handle isn't confirmed taken
// Network/timeout error: unknown state — don't block if the handle isn't confirmed unavailable
isAvailable = null;
setStatus('Could not check availability.', 'format-error');
updateSubmit();
Expand Down Expand Up @@ -714,7 +714,7 @@ export function renderChooseHandlePage(
setStatus('Could not check availability.', 'format-error');
randomBtn.disabled = false;
} else {
// Genuinely taken — retry with a new random value
// Genuinely unavailable — retry with a new random value
tryRandomHandle(attemptsLeft - 1);
}
})
Expand Down
2 changes: 1 addition & 1 deletion packages/demo/src/lib/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ function buildInjectedCss(
`.handle-row { border-color: ${page.inputBorder}; }`,
`.handle-suffix { color: ${page.textHint}; background: ${page.inputBg}; border-color: ${page.inputBorder}; }`,
'.status.available { color: #4ade80; }',
`.status.taken { color: ${page.errorText}; }`,
`.status.unavailable { color: ${page.errorText}; }`,
`.status.checking { color: ${page.textHint}; }`,
`.permissions { background: ${page.inputBg}; }`,
'.permissions li::before { color: #4ade80; }',
Expand Down
Loading