From 7fc0014d66e3d920b77dff00e440bc2471762eb1 Mon Sep 17 00:00:00 2001
From: Stephen Zhou <38493346+hyoban@users.noreply.github.com>
Date: Thu, 19 Mar 2026 11:18:28 +0800
Subject: [PATCH] tweaks
---
.../invite-settings/__tests__/page.spec.tsx | 111 ++++++++++++++++++
web/app/signin/invite-settings/page.tsx | 10 +-
2 files changed, 118 insertions(+), 3 deletions(-)
create mode 100644 web/app/signin/invite-settings/__tests__/page.spec.tsx
diff --git a/web/app/signin/invite-settings/__tests__/page.spec.tsx b/web/app/signin/invite-settings/__tests__/page.spec.tsx
new file mode 100644
index 0000000000..a13fba55a3
--- /dev/null
+++ b/web/app/signin/invite-settings/__tests__/page.spec.tsx
@@ -0,0 +1,111 @@
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import InviteSettingsPage from '../page'
+
+const mockReplace = vi.fn()
+const mockRefetch = vi.fn()
+const mockActivateMember = vi.fn()
+const mockSetLocaleOnClient = vi.fn()
+const mockResolvePostLoginRedirect = vi.fn()
+
+let mockInviteToken = 'invite-token'
+let mockCheckRes: {
+ is_valid: boolean
+ data: {
+ workspace_name: string
+ email: string
+ workspace_id: string
+ }
+} | undefined
+
+vi.mock('@/next/navigation', () => ({
+ useRouter: () => ({
+ replace: mockReplace,
+ }),
+ useSearchParams: () => ({
+ get: (key: string) => key === 'invite_token' ? mockInviteToken : null,
+ }),
+}))
+
+vi.mock('@/context/global-public-context', () => ({
+ useGlobalPublicStore: (selector: (state: { systemFeatures: { branding: { enabled: boolean } } }) => unknown) =>
+ selector({
+ systemFeatures: {
+ branding: {
+ enabled: true,
+ },
+ },
+ }),
+}))
+
+vi.mock('@/service/use-common', () => ({
+ useInvitationCheck: () => ({
+ data: mockCheckRes,
+ refetch: mockRefetch,
+ }),
+}))
+
+vi.mock('@/service/common', () => ({
+ activateMember: (...args: unknown[]) => mockActivateMember(...args),
+}))
+
+vi.mock('@/i18n-config', () => ({
+ setLocaleOnClient: (...args: unknown[]) => mockSetLocaleOnClient(...args),
+}))
+
+vi.mock('../../utils/post-login-redirect', () => ({
+ resolvePostLoginRedirect: () => mockResolvePostLoginRedirect(),
+}))
+
+describe('InviteSettingsPage', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mockInviteToken = 'invite-token'
+ mockCheckRes = undefined
+ mockActivateMember.mockResolvedValue({ result: 'success' })
+ mockSetLocaleOnClient.mockResolvedValue(undefined)
+ mockResolvePostLoginRedirect.mockReturnValue('/apps')
+ })
+
+ describe('Activation Gating', () => {
+ it('should not activate when invitation validation is still pending and Enter is pressed', () => {
+ render()
+
+ const nameInput = screen.getByLabelText('login.name')
+ fireEvent.change(nameInput, { target: { value: 'Alice' } })
+ fireEvent.keyDown(nameInput, { key: 'Enter', code: 'Enter', charCode: 13 })
+
+ expect(mockActivateMember).not.toHaveBeenCalled()
+ })
+
+ it('should activate when invitation validation has succeeded', async () => {
+ mockCheckRes = {
+ is_valid: true,
+ data: {
+ workspace_name: 'Demo Workspace',
+ email: 'alice@example.com',
+ workspace_id: 'workspace-1',
+ },
+ }
+
+ render()
+
+ const nameInput = screen.getByLabelText('login.name')
+ fireEvent.change(nameInput, { target: { value: 'Alice' } })
+ fireEvent.keyDown(nameInput, { key: 'Enter', code: 'Enter', charCode: 13 })
+
+ await waitFor(() => {
+ expect(mockActivateMember).toHaveBeenCalledWith({
+ url: '/activate',
+ body: {
+ token: 'invite-token',
+ name: 'Alice',
+ interface_language: 'en-US',
+ timezone: expect.any(String),
+ },
+ })
+ })
+ expect(mockSetLocaleOnClient).toHaveBeenCalledWith('en-US', false)
+ expect(mockReplace).toHaveBeenCalledWith('/apps')
+ })
+ })
+})
diff --git a/web/app/signin/invite-settings/page.tsx b/web/app/signin/invite-settings/page.tsx
index 16c398ad24..fd4c805f19 100644
--- a/web/app/signin/invite-settings/page.tsx
+++ b/web/app/signin/invite-settings/page.tsx
@@ -36,9 +36,12 @@ export default function InviteSettingsPage() {
},
}
const { data: checkRes, refetch: recheck } = useInvitationCheck(checkParams.params, !!token)
+ const canActivate = checkRes?.is_valid === true
const handleActivate = useCallback(async () => {
try {
+ if (!canActivate)
+ return
if (!name) {
Toast.notify({ type: 'error', message: t('enterYourName', { ns: 'login' }) })
return
@@ -62,7 +65,7 @@ export default function InviteSettingsPage() {
catch {
recheck()
}
- }, [language, name, recheck, timezone, token, router, t])
+ }, [canActivate, language, name, recheck, timezone, token, router, t])
if (checkRes?.is_valid === false) {
return (
@@ -104,7 +107,8 @@ export default function InviteSettingsPage() {
if (e.key === 'Enter') {
e.preventDefault()
e.stopPropagation()
- handleActivate()
+ if (canActivate)
+ handleActivate()
}
}}
/>
@@ -144,7 +148,7 @@ export default function InviteSettingsPage() {
variant="primary"
className="w-full"
onClick={handleActivate}
- disabled={!checkRes?.is_valid}
+ disabled={!canActivate}
>
{`${t('join', { ns: 'login' })} ${checkRes?.data?.workspace_name ?? ''}`}