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
27 changes: 27 additions & 0 deletions apps/web/src/shared/metrics/AnonymousCountsSetting.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// @vitest-environment jsdom
import '@testing-library/jest-dom/vitest'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
const { requireAuth } = vi.hoisted(() => ({ requireAuth: vi.fn() }))
vi.mock('../../features/auth/AuthGateContext', () => ({ useAuthGate: () => ({ isGated: true, requireAuth }) }))
vi.mock('../sound/interactionSounds', () => ({ playInterfaceSound: vi.fn() }))
import AuthInteractionGate from '../../features/auth/AuthInteractionGate'
import AnonymousCountsSetting from './AnonymousCountsSetting'
import { setAnonymousCountsEnabled } from './anonymousCounts'
afterEach(() => { cleanup(); requireAuth.mockClear() })
describe('anonymous opt-out with production guest gating active', () => {
it.each(['en', 'zh'])('lets a signed-out visitor opt out in %s without unlocking other actions', language => {
setAnonymousCountsEnabled(true)
const privateAction = vi.fn()
render(<AuthInteractionGate><AnonymousCountsSetting language={language} /><button onClick={privateAction}>Private action</button></AuthInteractionGate>)
const control = screen.getByRole('switch')
expect(control).toBeChecked()
fireEvent.pointerDown(control); fireEvent.click(control)
expect(control).not.toBeChecked()
expect(localStorage.getItem('focusgo.anonymous-counts.enabled')).toBe('false')
expect(requireAuth).not.toHaveBeenCalled()
fireEvent.click(screen.getByText('Private action'))
expect(requireAuth).toHaveBeenCalledTimes(1)
expect(privateAction).not.toHaveBeenCalled()
})
})
2 changes: 1 addition & 1 deletion apps/web/src/shared/metrics/AnonymousCountsSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { anonymousCountsEnabled, setAnonymousCountsEnabled, subscribeAnonymousCo
export default function AnonymousCountsSetting({ language }: { language: string }) {
const enabled = useSyncExternalStore(subscribeAnonymousCounts, anonymousCountsEnabled, () => false)
const zh = language === 'zh'
return <section className="rounded-[var(--radius-lg)] border p-5" aria-labelledby="anonymous-counts-title">
return <section className="rounded-[var(--radius-lg)] border p-5" aria-labelledby="anonymous-counts-title" data-auth-preview-allowed="true">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expose the opt-out through the guest navigation path

For a signed-out production visitor entering Settings normally, this exemption is never reachable: SettingsRoute initializes activeSection to appearance at line 606, while the Legal TabsTrigger at lines 1109–1125 is itself inside AuthInteractionGate and lacks data-auth-preview-allowed, so the gate intercepts its role="tab" interaction before openSection can navigate. Only manually entering /workspace/settings/legal exposes this switch, and the new test misses the problem by rendering AnonymousCountsSetting directly; allow guest navigation to the Legal section or put the opt-out in an already reachable view.

Useful? React with 👍 / 👎.

<div className="flex items-center justify-between gap-4">
<h3 id="anonymous-counts-title" className="font-semibold">{zh ? '匿名使用计数' : 'Anonymous usage counts'}</h3>
<Switch aria-labelledby="anonymous-counts-title" checked={enabled} onCheckedChange={setAnonymousCountsEnabled} />
Expand Down
Loading