Skip to content

Commit 326a559

Browse files
chore(i18n): delete 92 message keys nothing reads from all ten locale packs (#26158)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9SgBCUABMtPXuje9dvRBdof)_ Part of #25921 (the dead i18n keys slice; the card's other items stay open there). Clause-②: yes (`TranslationKeys` narrows: the 92 dead keys of #25921 and `validation.formInvalid` from #26040; additive `InvitationSignUpPrompt` export from #26044; patch per the objectui#4730 key-retirement precedent) Metadata-protocol: none ## i18n batch (seat `domain:ui#1`) This PR also carries three already-reviewed PRs on the same locale packs, merged in rather than landed one by one, so `scripts/sdui-manifest.sources/i18n.json` is re-recorded once and the merge queue runs once. The queue squash-merges, so stacked PRs would still conflict. The source PRs are closed as superseded when this merges. | source PR | card | packages | its review | |---|---|---|---| | #26040 | 25874 (form refusal focuses the first invalid field) | components, i18n | seat 1 ACCEPT | | #26044 | 25886 (invitation arrival copy) | app-shell, console, i18n | seat 2 ACCEPT + contract review 6413489249 | | #26048 | 25889 (map style failure notice) | plugin-map, i18n | seat 2 ACCEPT | | this branch | 25921 (92 dead keys) | i18n | seat 1 | Each source PR keeps its own changeset. The non-locale files are byte-identical to each source PR's head (checked by the seat), and the diff against `main` is exactly the union of the four PRs' files plus `scripts/sdui-manifest.sources/{components,i18n,plugin-map}.json`. Cards 25874, 25886 and 25889 are referenced without a closing keyword on purpose: each seat closes out its own card after this lands. ## What Deletes 92 message keys that nothing reads from all ten locale packs under `ui/packages/i18n/src/locales/` (`ar`, `de`, `en`, `es`, `fr`, `ja`, `ko`, `pt`, `ru`, `zh`). Pure deletions: no key is renamed or added, and no value changes. Each pack loses the same 92 leaves (`en` shows 93 deleted lines because `publicForm.unavailableDescription` spans two). Two test ratchets that name keys follow the deletion in the same PR: - `ui/packages/i18n/src/__tests__/objectView-config-keys-retired-4730.test.ts`: `console.objectView.ufTabs` and `viewTypeUnavailableShort` move from the surviving half (105 to 103) to the retired list (124 to 126). The total stays 229, and the retired list now pins both keys absent from every pack. - `ui/packages/app-shell/src/__tests__/product-specific.allowlist.json`: `workspace.invite` and `workspace.members` are deleted from the live list, as its own test asks ("a migrated one is deleted from the live list"). The list takes deletions only, and this is one. ## How each key was proved unused Measured on `origin/main` `6d15acab` (objectstack-ai/objectstack), with three searches per key: 1. `node ui/scripts/check-i18n-dead-keys.mjs --json`: every key below is in its CONFIRMED tier (no `t()` / `tt()` call site, no dynamic template head that reaches it, no indirect one-hop template, and no key-boundary textual occurrence of the dotted key or its property chain anywhere in `ui/`). The run reported 3,879 pack keys, 398 candidates, 122 CONFIRMED, 276 NEEDS-REVIEW, `wrongVerdicts: []` and `classFourUncovered: []`. 2. A whole-repository search, wider than the tool's `ui/` scope: `git grep -n -F -f KEYS -- . ':!ui/packages/i18n/src/locales'`, then a filter that keeps a hit only where the key is bounded on both sides by a character that cannot continue a dotted key. Result: 0 hits for all 92 keys. The one raw line it printed is `object-grid.rowHeight` in a `packages/spec` comment, a component name and not `grid.rowHeight`. Positive control: the same search for `preview.changes.typeCount.object` finds `DraftChangesPanel.tsx`. 3. A read of the subtree and dynamic pack readers the tool reports but cannot tier (its classes 1 and 5): `LoadingScreen.tsx` spreads `console.error` but reads only `connectionFailed` and `checkServer`; `outboundAgentText.ts` indexes `console.ai` only by its four `OutboundAgentTextKey` names; `flowResponse.ts` reads only `form.noPermissionToSave`. None of them reads a deleted key. A probe for template heads (a template literal or a string concatenation that starts with the key's parent namespace) over all non-test source in `ui/packages`, `ui/apps`, `ui/examples` and `ui/e2e` found one, a `table.` template inside an error message in `ui/packages/types/src/zod/objectql.zod.ts`, which is refusal text and not a translation lookup. After the change the tool reports 3,787 pack keys, 30 CONFIRMED (the 30 kept on purpose, listed below) and the same 276 NEEDS-REVIEW, so the deletion moved no key into either tier. ### The 92 deleted keys - `appDesigner`: `addComponent`, `aggregate`, `appearance`, `colorVariant`, `componentProperties`, `createApp`, `editApp`, `fields`, `livePreview`, `modeCode`, `modePreview`, `navBuilder`, `rowHeight`, `selectObjects`, `showFilters`, `showSearch`, `showSort`, `toolbar`, `valueField`, `viewType` - `console.ai`: `conversationReady`, `preparingConversation`, `workspaceSubtitle`, `workspaceTitle` - `console.commandPalette`: `createApp` - `console.error`: `serverUnreachable`, `timeout` - `console.objectView`: `ufTabs`, `viewTypeUnavailableShort` - `console`: `title` - `dashboard`: `addWidget`, `editLayout`, `resetLayout`, `saveLayout` - `errors`: `networkError`, `notFound`, `serverError`, `timeout` - `form`: `deleteSuccess`, `invalidFormat`, `removeItem`, `saveSuccess` - `grid`: `rowHeight` - `home`: `allApps`, `createFirstApp`, `title`, `welcomeDescription` - `home.gettingStarted`: `cta`, `title` - `kanban`: `deleteCard`, `deleteColumn` - `layout.appSwitcher`: `addApp`, `editApp`, `editNavigation`, `home`, `manageAllApps`, `noAppsConfigured`, `systemConsole` - `publicForm`: `consentLabelDefault`, `consentLink`, `retry`, `secureNotice`, `submitAnother`, `tryDemo`, `unavailableDescription`, `unavailableTitle` - `report`: `allLabel`, `columnTotal`, `columnsLabel`, `emptyLabel`, `failedToLoad`, `grandTotal`, `needsAcross`, `rowTotal`, `rowsLabel` - `sidebar`: `scope`, `settings` - `table`: `collapseRow`, `expandRow`, `exportExcel`, `filterColumn`, `freezeColumn`, `showing`, `unfreezeColumn` - `user`: `settings` - `validation`: `type` - `view`: `activeFilters`, `activeSort`, `saveAsView`, `unsavedChanges` - `workspace`: `invite`, `members` ## Kept although the tool lists them CONFIRMED (30) - **28 plural forms.** `preview.changes.typeCount.{object,view,page,dashboard,app,action,report,flow,permission,agent,email_template}_one` / `_other`, `notifications.arrivalMany_one` / `_other`, `notifications.arrivalRepeats_one` / `_other`, `fields.relativeDate.overdue_one` / `_other`. Their base keys are called with `count` (`DraftChangesPanel.tsx`, `useInboxArrivalNotifier.ts`, `core/src/utils/date-display.ts`), so i18next reads exactly these suffixed forms. The tool expands plural suffixes only for call sites it classifies as pack calls; these call sites reach the base key through a passed-in translator, so the base key lands in NEEDS-REVIEW and its suffixed forms fall to CONFIRMED. Deleting them would turn every count string on those screens back to the bare fallback. - **`console.ai.agentLabels.dataChat` and `metadataAssistant`.** No code reads them (`AiChatPage.tsx` maps only `ask` and `build`), but the pack marks them "Legacy keys kept for back-compat with any external overrides". Dropping them is a separate call. ## Acceptance notes - `ui/scripts/check-i18n-dead-keys.mjs` false-positive class, not fixed here: the plural forms of a base key that is called with `count` through a translator the AST pass does not classify as a pack call are reported CONFIRMED. 28 of today's 122 CONFIRMED entries (23%) are this class. Carrier: none. - `ui/scripts/check-i18n-dead-keys.mjs`'s text footprint does not see a key that a JSON list spells as namespace plus leaf (`product-specific.allowlist.json`): `workspace.invite` and `workspace.members` were CONFIRMED while that list named them. Carrier: none. - The pack objects' TypeScript types (`typeof en`, `TranslationKeys`) lose the deleted members. Nothing in this repository reads one of them by property (search 2 above covers the property chains too, and the tool's property-chain leg found none); an external host that does would see a type error. Recorded here because the claim line says no exported type changes. - `scripts/sdui-manifest.sources/i18n.json` is re-recorded (`node scripts/gen-sdui-manifest-node.mjs` after merging `main` at `a335f1de`), because `@object-ui/i18n` is in the console registry closure; `node scripts/check-sdui-manifest.mjs` is green on the pushed head. The i18n packs are serialised: #26045, #26040, #26044, #26048 and #26055 land first, and #26040 also edits these packs. When its turn comes this branch merges `main` again, re-records the digest and rebuilds the Console; the seat enqueues it. It is not armed or queued from here. Local test runs and gate readings are in the `os-dev-report` comment on #25921. --- _Generated by [Claude Code](https://claude.ai/code/session_01FpUtgwNrSiRgWJ8GAepPeP)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FpUtgwNrSiRgWJ8GAepPeP --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent df8aac4 commit 326a559

42 files changed

Lines changed: 1126 additions & 1078 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@object-ui/components': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
fix(components): a refused submit moves focus to the first invalid field and no longer toasts the field list over the dialog's title
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
- **Before.** A submit refused by field validation, on the client or by the server's `fields[]`, raised an error toast naming the fields ("Please check the highlighted fields: First name, Last name, Company…") on top of the message already written under each input. The toast cut its list after three names, and in a record create / edit dialog it was drawn over the title row.
12+
- **Now.** The form raises no toast for a refusal that every field carries inline. It scrolls to the first invalid field in form order and focuses it, and first activates that field's tab when it is on a background tab. The message under each input is the whole report.
13+
- **A field with nothing drawn to carry it.** When the server rejects a field that has no drawn input, for example one on a tab or section a `visibleWhen` predicate hides, the form shows the form-level banner and its toast with the server's sentence, the same path a rejection naming an unknown field already takes. The hidden tab is not activated.
14+
- **Earlier outcomes retire.** A refusal on the client now takes down the previous attempt's banner and toast, and a clear-on-hide notice, as an attempt that passes validation already did. Before, the refusal toast replaced them under the form's outcome id.
15+
- **`validation.formInvalid` removed.** The toast's sentence ("Please check the highlighted fields: {{fields}}") had no other reader, so it leaves the form's default map and all ten `@object-ui/i18n` locale packs. `validation.formInvalidJoiner` stays: the clear-on-hide notice, the faulted-`visibleWhen` refusal and the option-group hint still join field labels with it. A host bundle that overrides `validation.formInvalid` can drop the entry; nothing reads it.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/console': patch
4+
'@object-ui/i18n': patch
5+
---
6+
7+
fix(app-shell,console): an invitee who opens the invitation link signed out is greeted for the invitation, offered "Create an account and join" first, and the accept page names the inviter
8+
9+
Clause-②: yes
10+
Metadata-protocol: none
11+
12+
- **Login page.** A signed-out visitor arriving at `/login?redirect=/accept-invitation/ID` used to see the returning-user card ("Welcome back · Sign in to continue your work") with a small "Sign up" link under the form. Both login pages (the console's own and `DefaultLoginPage`) now title the card "Accept your invitation · Use the email address the invitation was sent to." Where the server offers sign-up to this visitor, a full-width "Create an account and join" link (the new `InvitationSignUpPrompt` export) sits above the sign-in form in place of the small link. With sign-up switched off, the visitor gets the invitation header and the sign-in form only.
13+
- **Register page.** From an invitation redirect, both register pages title the form "Create an account to join" and ask for the address the invitation was sent to, because only that account can accept it.
14+
- **Accept page.** `DefaultAcceptInvitationPage` shows an "Invited by" row with the inviting member's email, which `GET /organization/get-invitation` already returns as `inviterEmail`. That route returns no inviter name.
15+
- **Not in this change.** The signed-out pages name no organization, inviter or invited address, and do not prefill the email. The only invitation read the server serves answers a signed-out caller `401`, so a preview for signed-out visitors needs a new server read first.
16+
- New locale keys in all ten packs: `auth.login.invitationTitle`, `auth.login.invitationDescription`, `auth.login.invitationSignUpButton`, `auth.login.invitationSignInDivider`, `auth.register.invitationTitle`, `auth.register.invitationDescription`, `organization.accept.invitedBy`.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@object-ui/plugin-map': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
fix(plugin-map): a map whose style or tiles fail to load shows the record list with a translated notice, instead of a grey canvas under an English alert
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
- **`@object-ui/plugin-map`:** when MapLibre reports a style or tile load failure (an intranet or proxied deployment that cannot reach the tile host, an unreachable style URL), the map view now removes the map and lists its records in its place, the same list a browser without WebGL2 already got: searchable, and each row opens the record the way its marker did. A "Try the map again" button brings the map back. The notice above the list keeps MapLibre's reason and names a remedy that exists: a view with its own map style is told to check that URL, and a view on the public default style is told to ask an administrator to set a map style for the view. It no longer says the markers are "listed in the search box" or asks for a "custom map style for production use" setting that does not exist. That notice, the WebGL2 notice, the list's label and empty text, and the "records excluded for missing or invalid coordinates" count now read the translation bundle, so a zh-CN console shows them in Chinese.
12+
- **`@object-ui/i18n`:** eight new `element.map.*` keys in all ten packs.
13+
14+
What to do: nothing. A deployment-wide map style setting is not part of this change.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@object-ui/i18n': patch
3+
---
4+
5+
chore(i18n): delete 92 message keys nothing reads from all ten locale packs
6+
7+
Clause-②: yes (TranslationKeys narrows by 92 unread members; patch per the #4730 retirement precedent)
8+
Metadata-protocol: none
9+
10+
`ui/scripts/check-i18n-dead-keys.mjs` listed 122 keys with no call site and no textual occurrence anywhere in the repository. 92 of them are deleted from `ar`, `de`, `en`, `es`, `fr`, `ja`, `ko`, `pt`, `ru` and `zh`; no key is renamed or added, and no screen changes, because no code asked for any of them. The largest groups are `appDesigner.*` (20 keys), `report.*` (9), `publicForm.*` (8), `table.*` (7) and `layout.appSwitcher.*` (7).
11+
12+
Two groups the script also listed stay:
13+
14+
- 28 `_one` / `_other` plural forms (`preview.changes.typeCount.*`, `notifications.arrivalMany` / `arrivalRepeats`, `fields.relativeDate.overdue`). Their base keys are called with `count`, and i18next reads the plural form, so they are in use.
15+
- `console.ai.agentLabels.dataChat` and `metadataAssistant`, which the pack keeps on purpose for external overrides.
16+
17+
`TranslationKeys` (`typeof en`) and the other pack objects' types lose the 92 members. A host that reads one of them by property gets a type error; nothing in this repository does, and no `t()` call anywhere asks for one.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"package":"ui/packages/components","sources":226,"sha256":"7c50070c8bc750d5717fce0a081038855777bfb749567757047e672262517886","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
1+
{"package":"ui/packages/components","sources":226,"sha256":"cd1d23767a76e6b4dcf2c0d4ca61b10ba27b336eeab990b22bfb0f7d10bb0424","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"package":"ui/packages/i18n","sources":31,"sha256":"57909816bcbfa5661852ecf652392b19d6a1e971a608b07360bfa2399bca11e4","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
1+
{"package":"ui/packages/i18n","sources":31,"sha256":"ae5a36bc2327b985cb909eed35e099d1b2d953b7c4cb7dda463e4d8d3ef5cc86","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"package":"ui/packages/plugin-map","sources":4,"sha256":"9987a8a9895284b5e3c44407a44f284462c5f522616cf9f88f9d9bdd7e0b8d90","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
1+
{"package":"ui/packages/plugin-map","sources":4,"sha256":"7e7bcc627a2cf9334b56fe20b1b97ab2732eec9f1e5fe77ede4e570bde95d6b1","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}

‎ui/apps/console/src/pages/auth/LoginPage.tsx‎

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@
2626
* (objectui#11806). The sign-in request goes to the same server, so a
2727
* live-looking form would only have deferred that news to the submit.
2828
*
29+
* A visitor who arrived signed out from an invitation link
30+
* (`?redirect=/accept-invitation/ID`) is greeted for that invitation instead
31+
* of as a returning user, and, where sign-up is offered, gets app-shell's
32+
* `InvitationSignUpPrompt` above the form in place of the small "Sign up"
33+
* link — the same prompt `DefaultLoginPage` shows (#25886).
34+
*
2935
* Layout (objectui#12600): one 400 px column — brand mark, the card, a footer.
3036
* The card carries this page's own two-line header, so `<LoginForm>` renders
3137
* with `hideHeader`. On a dev instance the card's last row offers a one-click
@@ -54,6 +60,7 @@ import {
5460
needsBootstrapProbe,
5561
useBootstrapStatus,
5662
LocaleSwitcher,
63+
InvitationSignUpPrompt,
5764
} from '@object-ui/app-shell';
5865
import { AuthLayout } from './AuthLayout';
5966
import { followOauthAuthorize } from './followAuthorize';
@@ -304,6 +311,7 @@ export function LoginPage() {
304311
? `/register?redirect=${encodeURIComponent(redirect)}`
305312
: '/register';
306313
const legalLinks = readLegalLinks(authConfig);
314+
const offerSignUp = configRead === 'known' && signUpOffer === 'form';
307315

308316
return (
309317
<AuthLayout formWidth="base">
@@ -345,18 +353,28 @@ export function LoginPage() {
345353
<>
346354
<div className="mb-5">
347355
<h1 className="text-xl font-semibold tracking-tight text-foreground">
348-
{t('auth.login.welcomeTitle', { defaultValue: 'Welcome back' })}
356+
{invitationRedirect
357+
? t('auth.login.invitationTitle', { defaultValue: 'Accept your invitation' })
358+
: t('auth.login.welcomeTitle', { defaultValue: 'Welcome back' })}
349359
</h1>
350360
<p className="mt-1 text-[13px] text-muted-foreground">
351-
{t('auth.login.welcomeDescription', {
352-
defaultValue: 'Sign in to continue your work',
353-
})}
361+
{invitationRedirect
362+
? t('auth.login.invitationDescription', {
363+
defaultValue: 'Use the email address the invitation was sent to.',
364+
})
365+
: t('auth.login.welcomeDescription', {
366+
defaultValue: 'Sign in to continue your work',
367+
})}
354368
</p>
355369
</div>
370+
{invitationRedirect && offerSignUp ? (
371+
<div className="mb-5">
372+
<InvitationSignUpPrompt registerUrl={registerUrl} />
373+
</div>
374+
) : null}
356375
<LoginFormCard
357-
registerUrl={
358-
configRead === 'known' && signUpOffer === 'form' ? registerUrl : undefined
359-
}
376+
// An invitee's sign-up is the prompt above, not a second link.
377+
registerUrl={offerSignUp && !invitationRedirect ? registerUrl : undefined}
360378
redirect={redirect}
361379
/>
362380
{devSeedAdmin ? <DevAdminRow seed={devSeedAdmin} /> : null}

‎ui/apps/console/src/pages/auth/RegisterPage.tsx‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,10 +257,23 @@ export function RegisterPage() {
257257
<AuthLayout formWidth="md">
258258
<Card className="border-border/60 px-4 py-8 shadow-sm shadow-primary/5 backdrop-blur supports-[backdrop-filter]:bg-card/95">
259259
<RegisterForm
260-
title={t('auth.register.title', { defaultValue: 'Create an account' })}
261-
description={t('auth.register.description', {
262-
defaultValue: 'Create your account to start building.',
263-
})}
260+
// #25886 — an invitee is told to register the invited address: the
261+
// invitation is accepted only by the account that carries it.
262+
title={
263+
invitationRedirect
264+
? t('auth.register.invitationTitle', { defaultValue: 'Create an account to join' })
265+
: t('auth.register.title', { defaultValue: 'Create an account' })
266+
}
267+
description={
268+
invitationRedirect
269+
? t('auth.register.invitationDescription', {
270+
defaultValue:
271+
'Register with the email address the invitation was sent to. The invitation can only be accepted with that address.',
272+
})
273+
: t('auth.register.description', {
274+
defaultValue: 'Create your account to start building.',
275+
})
276+
}
264277
loginUrl={loginUrl}
265278
verificationCallbackURL={verificationCallbackURL}
266279
linkComponent={RouterLink}

‎ui/apps/console/src/pages/auth/__tests__/signUpFollowsPosture-11691.test.tsx‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ import { RegisterPage } from '../RegisterPage';
4949

5050
const AUTH_URL = 'http://localhost/api/v1/auth';
5151
const SIGN_UP_LINK = { name: 'Sign up' } as const;
52+
/** #25886 — an invitee's sign-up is the register-first prompt, not the small link. */
53+
const INVITE_SIGN_UP = { name: builtInLocales.en.auth.login.invitationSignUpButton } as const;
5254
const INVITATION = '/accept-invitation/inv_1';
5355
const INVITE_QUERY = `?redirect=${encodeURIComponent(INVITATION)}`;
5456
const SELF_REGISTRATION_CLOSED_TEXT = builtInLocales.en.auth.register.errors.selfRegistrationClosed;
@@ -162,11 +164,20 @@ describe('LoginPage — the "Sign up" link follows the audience posture (objectu
162164
expect(screen.queryByRole('link', SIGN_UP_LINK)).toBeNull();
163165
});
164166

165-
it('under invite_only, still offers "Sign up" to an invitation redirect, carrying the redirect', async () => {
167+
it('under invite_only, still offers sign-up to an invitation redirect, carrying the redirect', async () => {
166168
renderAt(`/login${INVITE_QUERY}`, configFor('invite_only'));
167169
await loginConfigApplied();
168170

169-
expect(screen.getByRole('link', SIGN_UP_LINK).getAttribute('href')).toBe(`/register${INVITE_QUERY}`);
171+
// #25886 — greeted for the invitation, not as a returning user, with the
172+
// register-first prompt in place of the small link, never both.
173+
expect(
174+
screen.getByRole('heading', { name: builtInLocales.en.auth.login.invitationTitle }),
175+
).toBeInTheDocument();
176+
expect(
177+
screen.queryByRole('heading', { name: builtInLocales.en.auth.login.welcomeTitle }),
178+
).toBeNull();
179+
expect(screen.getByRole('link', INVITE_SIGN_UP).getAttribute('href')).toBe(`/register${INVITE_QUERY}`);
180+
expect(screen.queryByRole('link', SIGN_UP_LINK)).toBeNull();
170181
expect(wire.bootstrapProbes).toBe(0);
171182
});
172183

@@ -202,6 +213,11 @@ describe('LoginPage — the "Sign up" link follows the audience posture (objectu
202213
await loginConfigApplied();
203214

204215
expect(screen.queryByRole('link', SIGN_UP_LINK)).toBeNull();
216+
// #25886 — still greeted for the invitation, but offered no registration.
217+
expect(
218+
screen.getByRole('heading', { name: builtInLocales.en.auth.login.invitationTitle }),
219+
).toBeInTheDocument();
220+
expect(screen.queryByRole('link', INVITE_SIGN_UP)).toBeNull();
205221
});
206222
});
207223

@@ -220,9 +236,13 @@ describe('RegisterPage — explains invitation-only registration before the form
220236
it('an invitation redirect reaches a working registration under invite_only, from /login on', async () => {
221237
renderAt(`/login${INVITE_QUERY}`, configFor('invite_only'));
222238
await loginConfigApplied();
223-
await userEvent.click(screen.getByRole('link', SIGN_UP_LINK));
239+
await userEvent.click(screen.getByRole('link', INVITE_SIGN_UP));
224240

225241
await userEvent.type(await screen.findByLabelText('Name'), 'Ada');
242+
// #25886 — the form asks for the invited address.
243+
expect(
244+
screen.getByRole('heading', { name: builtInLocales.en.auth.register.invitationTitle }),
245+
).toBeInTheDocument();
226246
await userEvent.type(screen.getByLabelText('Email'), 'ada@example.com');
227247
await userEvent.type(screen.getByLabelText('Password'), 'hunter2hunter2');
228248
await userEvent.type(screen.getByLabelText('Confirm Password'), 'hunter2hunter2');

0 commit comments

Comments
 (0)