Have an account? {' '}
({ toast: { error: vi.fn() } }))
+
+vi.mock('@/components/interfaces/RoleImpersonationSelector/UserImpersonationSelector', () => ({
+ UserImpersonationSelector: ({ disabled }: { disabled?: boolean }) => (
+
+
+ Project
+
+
+ ),
+}))
+
+describe('RoleImpersonationSelectorInterface', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('enables user settings for authenticated queries and summarizes each role', async () => {
+ const user = userEvent.setup()
+ let currentRole: ImpersonationRole | undefined
+ const setRole = vi.fn(async (role: ImpersonationRole | undefined) => {
+ currentRole = role
+ })
+ const state: RoleImpersonationController = {
+ get role() {
+ return currentRole
+ },
+ claims: undefined,
+ setRole,
+ }
+
+ const { rerender } = customRender(
)
+
+ expect(screen.queryByText('Role')).not.toBeInTheDocument()
+ for (const option of screen.getAllByRole('radio')) {
+ expect(option).toHaveClass('w-full')
+ expect(option.querySelector('svg')).toBeInTheDocument()
+ }
+ expect(screen.getByRole('radio', { name: 'PostgresSuperuser' })).toBeChecked()
+ expect(screen.getByText('Bypasses RLS and can return all rows.')).toBeVisible()
+ expect(screen.getByTestId('user-settings')).toBeDisabled()
+
+ await user.click(screen.getByRole('radio', { name: 'AuthenticatedLogged-in user' }))
+
+ expect(screen.getByText('Returns rows available to the selected user.')).toBeVisible()
+ expect(screen.getByTestId('user-settings')).not.toBeDisabled()
+
+ await user.click(screen.getByRole('radio', { name: 'AnonymousNot logged in' }))
+ rerender(
)
+
+ expect(screen.getByText('Returns rows available to anonymous users.')).toBeVisible()
+ expect(setRole).toHaveBeenCalledWith({ type: 'postgrest', role: 'anon' })
+ expect(screen.getByTestId('user-settings')).toBeDisabled()
+
+ await user.click(screen.getByRole('radio', { name: 'PostgresSuperuser' }))
+ rerender(
)
+
+ expect(screen.getByText('Bypasses RLS and can return all rows.')).toBeVisible()
+ expect(setRole).toHaveBeenCalledWith(undefined)
+ })
+
+ it('follows externally cleared role state', () => {
+ let currentRole: ImpersonationRole | undefined = {
+ type: 'postgrest',
+ role: 'authenticated',
+ userType: 'native',
+ }
+ const state: RoleImpersonationController = {
+ get role() {
+ return currentRole
+ },
+ claims: undefined,
+ setRole: vi.fn(),
+ }
+
+ const { rerender } = customRender(
)
+
+ expect(screen.getByRole('radio', { name: 'AuthenticatedLogged-in user' })).toBeChecked()
+ expect(screen.getByTestId('user-settings')).not.toBeDisabled()
+
+ currentRole = undefined
+ rerender(
)
+
+ expect(screen.getByRole('radio', { name: 'PostgresSuperuser' })).toBeChecked()
+ expect(screen.getByTestId('user-settings')).toBeDisabled()
+ })
+
+ it('allows switching roles while impersonating a user', async () => {
+ const user = userEvent.setup()
+ const setRole = vi.fn().mockResolvedValue(undefined)
+ const state: RoleImpersonationController = {
+ role: {
+ type: 'postgrest',
+ role: 'authenticated',
+ userType: 'external',
+ externalAuth: { sub: 'external-user', additionalClaims: {} },
+ },
+ claims: undefined,
+ setRole,
+ }
+
+ customRender(
)
+
+ await user.click(screen.getByRole('radio', { name: 'AnonymousNot logged in' }))
+
+ expect(setRole).toHaveBeenCalledWith({ type: 'postgrest', role: 'anon' })
+ })
+
+ it('keeps the current selection and reports rejected role switches', async () => {
+ const user = userEvent.setup()
+ const state: RoleImpersonationController = {
+ role: undefined,
+ claims: undefined,
+ setRole: vi.fn().mockRejectedValue({ message: 'Role switch failed' }),
+ }
+
+ customRender(
)
+ await user.click(screen.getByRole('radio', { name: 'AnonymousNot logged in' }))
+
+ expect(screen.getByRole('radio', { name: 'PostgresSuperuser' })).toBeChecked()
+ expect(toast.error).toHaveBeenCalledWith('Failed to impersonate user: Role switch failed')
+ })
+})
diff --git a/apps/studio/tests/components/SQLEditor/RoleImpersonationSelector.utils.test.ts b/apps/studio/tests/components/SQLEditor/RoleImpersonationSelector.utils.test.ts
new file mode 100644
index 0000000000000..ea2abda8b0768
--- /dev/null
+++ b/apps/studio/tests/components/SQLEditor/RoleImpersonationSelector.utils.test.ts
@@ -0,0 +1,74 @@
+import { describe, expect, it } from 'vitest'
+
+import {
+ getRoleSelectionUpdate,
+ getSelectedRoleOption,
+} from '@/components/interfaces/RoleImpersonationSelector/RoleImpersonationSelector.utils'
+import type { ImpersonationRole, PostgrestRole } from '@/lib/role-impersonation'
+
+describe('getSelectedRoleOption', () => {
+ const cases: Array<{
+ name: string
+ role: ImpersonationRole | undefined
+ expected: PostgrestRole
+ }> = [
+ { name: 'defaults to Postgres when no role is set', role: undefined, expected: 'service_role' },
+ {
+ name: 'maps the service role to Postgres',
+ role: { type: 'postgrest', role: 'service_role' },
+ expected: 'service_role',
+ },
+ {
+ name: 'maps custom roles to Postgres',
+ role: { type: 'custom', role: 'reporter' },
+ expected: 'service_role',
+ },
+ {
+ name: 'preserves the anonymous role',
+ role: { type: 'postgrest', role: 'anon' },
+ expected: 'anon',
+ },
+ {
+ name: 'preserves native authenticated roles',
+ role: {
+ type: 'postgrest',
+ role: 'authenticated',
+ userType: 'native',
+ },
+ expected: 'authenticated',
+ },
+ {
+ name: 'preserves external authenticated roles',
+ role: {
+ type: 'postgrest',
+ role: 'authenticated',
+ userType: 'external',
+ },
+ expected: 'authenticated',
+ },
+ ]
+
+ it.each(cases)('$name', ({ role, expected }) => {
+ expect(getSelectedRoleOption(role)).toBe(expected)
+ })
+})
+
+describe('getRoleSelectionUpdate', () => {
+ it('clears role impersonation for Postgres', () => {
+ expect(getRoleSelectionUpdate('service_role')).toEqual({
+ shouldSetRole: true,
+ role: undefined,
+ })
+ })
+
+ it('sets the anonymous PostgREST role', () => {
+ expect(getRoleSelectionUpdate('anon')).toEqual({
+ shouldSetRole: true,
+ role: { type: 'postgrest', role: 'anon' },
+ })
+ })
+
+ it('waits for a user selection before setting the authenticated role', () => {
+ expect(getRoleSelectionUpdate('authenticated')).toEqual({ shouldSetRole: false })
+ })
+})
diff --git a/apps/studio/tests/components/SQLEditor/UserImpersonationSelector.test.tsx b/apps/studio/tests/components/SQLEditor/UserImpersonationSelector.test.tsx
new file mode 100644
index 0000000000000..aad89d6383c76
--- /dev/null
+++ b/apps/studio/tests/components/SQLEditor/UserImpersonationSelector.test.tsx
@@ -0,0 +1,151 @@
+import { screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { mockAnimationsApi } from 'jsdom-testing-mocks'
+import { HttpResponse } from 'msw'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+import { UserImpersonationSelector } from '@/components/interfaces/RoleImpersonationSelector/UserImpersonationSelector'
+import type { AuthConfigResponse } from '@/data/auth/auth-config-query'
+import type { User } from '@/data/auth/users-infinite-query'
+import type { ProjectDetail } from '@/data/projects/project-detail-query'
+import type { RoleImpersonationController } from '@/state/role-impersonation-state'
+import { customRender } from '@/tests/lib/custom-render'
+import { addAPIMock } from '@/tests/lib/msw'
+
+mockAnimationsApi()
+
+const PROJECT: ProjectDetail = {
+ cloud_provider: 'AWS',
+ connectionString: 'postgresql://postgres@localhost:5432/postgres',
+ db_host: 'db.default.supabase.co',
+ high_availability: false,
+ id: 1,
+ inserted_at: '2026-01-01T00:00:00.000Z',
+ integration_source: null,
+ is_branch_enabled: false,
+ is_hibernating: false,
+ is_physical_backups_enabled: false,
+ name: 'Test project',
+ organization_id: 1,
+ ref: 'default',
+ region: 'us-east-1',
+ restUrl: 'https://default.supabase.co/rest/v1',
+ status: 'ACTIVE_HEALTHY',
+ subscription_id: 'subscription-1',
+ updated_at: '2026-01-01T00:00:00.000Z',
+}
+
+const AUTH_CONFIG_WITHOUT_CUSTOM_ACCESS_TOKEN_HOOK = {
+ HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: false,
+ HOOK_CUSTOM_ACCESS_TOKEN_SECRETS: '',
+ HOOK_CUSTOM_ACCESS_TOKEN_URI: '',
+} as unknown as AuthConfigResponse
+
+describe('UserImpersonationSelector', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+
+ addAPIMock({
+ method: 'get',
+ path: '/platform/projects/:ref',
+ response: PROJECT,
+ })
+ addAPIMock({
+ method: 'get',
+ path: '/platform/auth/:ref/config',
+ response: () =>
+ HttpResponse.json
(AUTH_CONFIG_WITHOUT_CUSTOM_ACCESS_TOKEN_HOOK),
+ })
+ addAPIMock({
+ method: 'post',
+ path: '/platform/pg-meta/:ref/query',
+ response: () => HttpResponse.json([]),
+ })
+ })
+
+ it('shows the external claims example and MFA guidance in the shared layout', async () => {
+ const user = userEvent.setup()
+ const state: RoleImpersonationController = {
+ role: {
+ type: 'postgrest',
+ role: 'authenticated',
+ userType: 'native',
+ },
+ claims: undefined,
+ setRole: vi.fn(),
+ }
+
+ customRender( )
+ await user.click(screen.getByRole('radio', { name: 'External' }))
+
+ expect(
+ screen.getByPlaceholderText('e.g. {"app_metadata": {"org_id": "org_456"}}')
+ ).toBeVisible()
+ expect(screen.getByText('MFA level')).toBeVisible()
+ expect(screen.getByRole('button', { name: 'Users' })).toBeVisible()
+ expect(screen.getByRole('button', { name: 'MFA level' })).toBeVisible()
+ })
+
+ it('clears the selected user when switching user sources', async () => {
+ const user = userEvent.setup()
+ const onUserImpersonationCleared = vi.fn()
+ const state: RoleImpersonationController = {
+ role: {
+ type: 'postgrest',
+ role: 'authenticated',
+ userType: 'external',
+ externalAuth: { sub: 'external-user', additionalClaims: {} },
+ aal: 'aal2',
+ },
+ claims: undefined,
+ setRole: vi.fn().mockResolvedValue(undefined),
+ }
+
+ customRender(
+
+ )
+
+ expect(screen.getByRole('radio', { name: 'Project' })).toBeEnabled()
+ expect(screen.getByRole('radio', { name: 'External' })).toBeEnabled()
+ expect(screen.getByRole('button', { name: 'Stop impersonating user' })).toBeEnabled()
+
+ await user.click(screen.getByRole('radio', { name: 'Project' }))
+
+ expect(state.setRole).toHaveBeenCalledWith(undefined)
+ expect(onUserImpersonationCleared).toHaveBeenCalledOnce()
+ })
+
+ it('updates the active impersonation when switching MFA levels', async () => {
+ const user = userEvent.setup()
+ const state: RoleImpersonationController = {
+ role: {
+ type: 'postgrest',
+ role: 'authenticated',
+ userType: 'external',
+ externalAuth: { sub: 'external-user', additionalClaims: {} },
+ aal: 'aal2',
+ },
+ claims: undefined,
+ setRole: vi.fn().mockResolvedValue(undefined),
+ }
+
+ customRender( )
+
+ expect(screen.getByRole('radio', { name: 'AAL1' })).toBeEnabled()
+ expect(screen.getByRole('radio', { name: 'AAL2' })).toBeEnabled()
+ expect(screen.getByRole('radio', { name: 'AAL2' })).toBeChecked()
+
+ await user.click(screen.getByRole('radio', { name: 'AAL1' }))
+
+ expect(state.setRole).toHaveBeenCalledWith(
+ expect.objectContaining({
+ aal: 'aal1',
+ externalAuth: { sub: 'external-user', additionalClaims: {} },
+ }),
+ undefined
+ )
+ })
+})
diff --git a/apps/studio/tests/components/Settings/Infrastructure/AddReadReplicaDialog.test.tsx b/apps/studio/tests/components/Settings/Infrastructure/AddReadReplicaDialog.test.tsx
new file mode 100644
index 0000000000000..aa14ac6d74d73
--- /dev/null
+++ b/apps/studio/tests/components/Settings/Infrastructure/AddReadReplicaDialog.test.tsx
@@ -0,0 +1,99 @@
+import { screen, waitFor } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { useState } from 'react'
+import { describe, expect, test, vi } from 'vitest'
+
+import { AddReadReplicaDialog } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/AddReadReplicaDialog'
+import { customRender } from '@/tests/lib/custom-render'
+
+const { mockReadReplicaForm } = vi.hoisted(() => ({ mockReadReplicaForm: vi.fn() }))
+
+vi.mock('@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicaForm', () => ({
+ ReadReplicaForm: ({
+ onClose,
+ onRecommendCompute,
+ }: {
+ onClose: () => void
+ onRecommendCompute: (size: 'ci_small') => void
+ }) => {
+ mockReadReplicaForm()
+
+ return (
+ <>
+ onRecommendCompute('ci_small')}>
+ Change compute
+
+
+ Change region
+
+
+ Cancel
+
+ >
+ )
+ },
+}))
+
+const renderDialog = (onRecommendCompute = vi.fn()) => {
+ const TestDialog = () => {
+ const [open, setOpen] = useState(true)
+
+ return (
+
+ )
+ }
+
+ return customRender( )
+}
+
+describe('AddReadReplicaDialog', () => {
+ test('does not load form data while closed', () => {
+ customRender(
+
+ )
+
+ expect(mockReadReplicaForm).not.toHaveBeenCalled()
+ })
+
+ test('closes an unchanged dialog without confirmation', async () => {
+ const user = userEvent.setup()
+ renderDialog()
+
+ expect(screen.getByRole('dialog', { name: 'Add read replica' })).toBeInTheDocument()
+
+ await user.click(screen.getByRole('button', { name: 'Close' }))
+
+ await waitFor(() => {
+ expect(screen.queryByRole('dialog', { name: 'Add read replica' })).not.toBeInTheDocument()
+ })
+ expect(screen.queryByText('Unsaved changes')).not.toBeInTheDocument()
+ })
+
+ test('closes after a transient region selection without confirmation', async () => {
+ const user = userEvent.setup()
+ renderDialog()
+
+ await user.click(screen.getByRole('button', { name: 'Change region' }))
+ await user.click(screen.getByRole('button', { name: 'Cancel' }))
+
+ await waitFor(() => {
+ expect(screen.queryByRole('dialog', { name: 'Add read replica' })).not.toBeInTheDocument()
+ })
+ expect(screen.queryByText('Unsaved changes')).not.toBeInTheDocument()
+ })
+
+ test('hands the recommendation off after closing the dialog', async () => {
+ const user = userEvent.setup()
+ const onRecommendCompute = vi.fn()
+
+ renderDialog(onRecommendCompute)
+
+ await user.click(screen.getByRole('button', { name: 'Change compute' }))
+
+ await waitFor(() => expect(onRecommendCompute).toHaveBeenCalledWith('ci_small'))
+ })
+})
diff --git a/apps/studio/tests/components/Settings/Infrastructure/AddReadReplicaSheet.test.tsx b/apps/studio/tests/components/Settings/Infrastructure/AddReadReplicaSheet.test.tsx
deleted file mode 100644
index d441169f843b6..0000000000000
--- a/apps/studio/tests/components/Settings/Infrastructure/AddReadReplicaSheet.test.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { screen, waitFor } from '@testing-library/react'
-import userEvent from '@testing-library/user-event'
-import { describe, expect, test, vi } from 'vitest'
-
-import { AddReadReplicaSheet } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/AddReadReplicaSheet'
-import { customRender } from '@/tests/lib/custom-render'
-
-vi.mock('@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicaForm', () => ({
- ReadReplicaForm: ({ onRecommendCompute }: { onRecommendCompute: (size: 'ci_small') => void }) => (
- onRecommendCompute('ci_small')}>
- Change to Small compute
-
- ),
-}))
-
-describe('AddReadReplicaSheet', () => {
- test('hands the recommendation off after closing the sheet', async () => {
- const user = userEvent.setup()
- const onRecommendCompute = vi.fn()
-
- customRender( , {
- nuqs: { searchParams: { addReplica: 'true' } },
- })
-
- await user.click(screen.getByRole('button', { name: 'Change to Small compute' }))
-
- await waitFor(() => expect(onRecommendCompute).toHaveBeenCalledWith('ci_small'))
- })
-})
diff --git a/apps/studio/tests/components/Settings/Infrastructure/ReadReplicaEligibilityWarnings.test.tsx b/apps/studio/tests/components/Settings/Infrastructure/ReadReplicaEligibilityWarnings.test.tsx
index fe468736f156b..5bd41bd2c3140 100644
--- a/apps/studio/tests/components/Settings/Infrastructure/ReadReplicaEligibilityWarnings.test.tsx
+++ b/apps/studio/tests/components/Settings/Infrastructure/ReadReplicaEligibilityWarnings.test.tsx
@@ -3,13 +3,9 @@ import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import { ReadReplicaEligibilityWarnings } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicaForm/ReadReplicaEligibilityWarnings'
-import { useCheckEligibilityDeployReplica } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicaForm/useCheckEligibilityDeployReplica'
import { READ_REPLICAS_MAX_COUNT } from '@/data/read-replicas/replicas-query'
import { customRender } from '@/tests/lib/custom-render'
-vi.mock(
- '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicaForm/useCheckEligibilityDeployReplica'
-)
vi.mock('@/data/projects/project-detail-query', () => ({
useProjectDetailQuery: () => ({ data: undefined, isSuccess: false }),
}))
@@ -41,33 +37,36 @@ describe('ReadReplicaEligibilityWarnings – below small compute', () => {
it('recommends Small compute when project is on pico, nano, or micro compute', async () => {
const user = userEvent.setup()
const onRecommendCompute = vi.fn()
- vi.mocked(useCheckEligibilityDeployReplica).mockReturnValue(
- eligibility({ isBelowSmallCompute: true })
- )
+ const warningEligibility = eligibility({ isBelowSmallCompute: true })
- customRender( )
+ customRender(
+
+ )
+ expect(screen.getByText('Small compute required')).toBeInTheDocument()
expect(
- screen.getByText('Project required to at least be on a Small compute')
- ).toBeInTheDocument()
- expect(
- screen.getByText(
- 'This is to ensure that read replicas can keep up with the primary database’s activities.'
- )
+ screen.getByText(/Read replicas require at least Small compute to keep up/)
).toBeInTheDocument()
- expect(screen.getByRole('button', { name: /change to small compute/i })).toBeInTheDocument()
- await user.click(screen.getByRole('button', { name: /change to small compute/i }))
+ expect(screen.getByRole('link', { name: 'Learn more' })).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'Change compute' })).toBeInTheDocument()
+ await user.click(screen.getByRole('button', { name: 'Change compute' }))
expect(onRecommendCompute).toHaveBeenCalledWith('ci_small')
})
})
describe('ReadReplicaEligibilityWarnings – max replicas reached', () => {
it('shows upsell to upgrade compute when below the default cap (e.g. ci_small/medium/large → 4 replicas)', () => {
- vi.mocked(useCheckEligibilityDeployReplica).mockReturnValue(
- eligibility({ isReachedMaxReplicas: true, maxNumberOfReplicas: 4 })
- )
+ const warningEligibility = eligibility({ isReachedMaxReplicas: true, maxNumberOfReplicas: 4 })
- customRender( )
+ customRender(
+
+ )
expect(
screen.getByText('You can only deploy up to 4 read replicas at once')
@@ -77,11 +76,17 @@ describe('ReadReplicaEligibilityWarnings – max replicas reached', () => {
})
it('does NOT show the compute upsell when already at the default cap (XL+)', () => {
- vi.mocked(useCheckEligibilityDeployReplica).mockReturnValue(
- eligibility({ isReachedMaxReplicas: true, maxNumberOfReplicas: READ_REPLICAS_MAX_COUNT })
- )
+ const warningEligibility = eligibility({
+ isReachedMaxReplicas: true,
+ maxNumberOfReplicas: READ_REPLICAS_MAX_COUNT,
+ })
- customRender( )
+ customRender(
+
+ )
expect(
screen.getByText(`You can only deploy up to ${READ_REPLICAS_MAX_COUNT} read replicas at once`)
diff --git a/apps/studio/tests/components/Settings/Infrastructure/ReadReplicaPricingDialog.test.tsx b/apps/studio/tests/components/Settings/Infrastructure/ReadReplicaPricingDialog.test.tsx
new file mode 100644
index 0000000000000..1387ddea447d7
--- /dev/null
+++ b/apps/studio/tests/components/Settings/Infrastructure/ReadReplicaPricingDialog.test.tsx
@@ -0,0 +1,65 @@
+import { screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { describe, expect, test, vi } from 'vitest'
+
+import { ReadReplicaPricingDialog } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicaForm/ReadReplicaPricingDialog'
+import { customRender } from '@/tests/lib/custom-render'
+
+vi.mock('@/hooks/misc/useSelectedProject', () => ({
+ useSelectedProjectQuery: () => ({ data: { cloud_provider: 'AWS' } }),
+}))
+
+const replicaCost = {
+ isLoading: false,
+ isError: false,
+ retry: vi.fn(),
+ totalCost: '$85.94',
+ compute: { label: 'Small', cost: '$15.00', priceDescription: '$15/month' },
+ disk: { type: 'gp3', label: '125 GB (gp3)', cost: '$15.63' },
+ iops: { label: '3,000 IOPS', cost: '$30.00' },
+ throughput: { label: '125 MB/s', cost: '$25.31' },
+} as const
+
+describe('ReadReplicaPricingDialog', () => {
+ test('does not show a partial estimate while pricing data loads', () => {
+ customRender( )
+
+ expect(screen.getByText('Estimated additional cost')).toBeInTheDocument()
+ expect(screen.queryByText(/Estimated additional cost of/)).not.toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'View breakdown' })).toBeDisabled()
+ })
+
+ test('shows the estimate once all pricing data is available', () => {
+ customRender( )
+
+ expect(screen.getByText('Estimated additional cost of $85.94/month')).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'View breakdown' })).toBeEnabled()
+ })
+
+ test('offers to retry when pricing data cannot be loaded', async () => {
+ const user = userEvent.setup()
+ const retry = vi.fn()
+
+ customRender(
+
+ )
+
+ expect(screen.getByText('Unable to estimate additional cost')).toBeInTheDocument()
+ expect(screen.getByText('We couldn’t load the required pricing data.')).toBeInTheDocument()
+
+ await user.click(screen.getByRole('button', { name: 'Retry' }))
+
+ expect(retry).toHaveBeenCalledOnce()
+ })
+
+ test('keeps the error state stable while retrying', () => {
+ customRender(
+
+ )
+
+ expect(screen.getByText('Unable to estimate additional cost')).toBeInTheDocument()
+ expect(screen.queryByText('Estimated additional cost')).not.toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'Retry' })).toBeDisabled()
+ expect(screen.queryByRole('button', { name: 'View breakdown' })).not.toBeInTheDocument()
+ })
+})
diff --git a/apps/studio/tests/components/Settings/Infrastructure/ReadReplicasSection.test.tsx b/apps/studio/tests/components/Settings/Infrastructure/ReadReplicasSection.test.tsx
index cf9a305968c12..f8b1928b81225 100644
--- a/apps/studio/tests/components/Settings/Infrastructure/ReadReplicasSection.test.tsx
+++ b/apps/studio/tests/components/Settings/Infrastructure/ReadReplicasSection.test.tsx
@@ -18,6 +18,10 @@ const { mockUseIsFeatureEnabled } = vi.hoisted(() => ({
vi.mock('@/hooks/misc/useIsFeatureEnabled', () => ({
useIsFeatureEnabled: mockUseIsFeatureEnabled,
}))
+vi.mock(
+ '@/components/interfaces/Settings/Infrastructure/ReadReplicas/AddReadReplicaDialog',
+ () => ({ AddReadReplicaDialog: () => null })
+)
const addReplicaListMocks = () => {
addAPIMock({
diff --git a/apps/studio/tests/pages/project/[ref]/settings/infrastructure.test.tsx b/apps/studio/tests/pages/project/[ref]/settings/infrastructure.test.tsx
index babf74a4799fd..484e844616c8d 100644
--- a/apps/studio/tests/pages/project/[ref]/settings/infrastructure.test.tsx
+++ b/apps/studio/tests/pages/project/[ref]/settings/infrastructure.test.tsx
@@ -339,12 +339,12 @@ describe('/project/[ref]/settings/infrastructure', () => {
})
})
- test('focuses the recommended compute option after closing the add replica sheet', async () => {
+ test('focuses the recommended compute option after closing the add replica dialog', async () => {
const user = userEvent.setup()
renderInfrastructurePage()
await user.click(await screen.findByRole('button', { name: 'Add read replica' }))
- await user.click(await screen.findByRole('button', { name: 'Change to Small compute' }))
+ await user.click(await screen.findByRole('button', { name: 'Change compute' }))
const smallCompute = await screen.findByRole('radio', { name: /Small/ })
await waitFor(() => expect(smallCompute).toHaveFocus())
diff --git a/apps/studio/tests/pages/sign-up.test.tsx b/apps/studio/tests/pages/sign-up.test.tsx
new file mode 100644
index 0000000000000..d305bae88f183
--- /dev/null
+++ b/apps/studio/tests/pages/sign-up.test.tsx
@@ -0,0 +1,101 @@
+import { screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { useState } from 'react'
+import { Button } from 'ui'
+import { beforeEach, describe, expect, test, vi } from 'vitest'
+
+import {
+ CHATGPT_IDENTITY_PROVIDER,
+ GITHUB_IDENTITY_PROVIDER,
+ type ExternalIdentityProviderConfig,
+} from '@/lib/external-identity-providers'
+import SignUpPage from '@/pages/sign-up'
+import { customRender } from '@/tests/lib/custom-render'
+
+const mocks = vi.hoisted(() => ({
+ focusProvider: undefined as ExternalIdentityProviderConfig | undefined,
+}))
+
+vi.mock('@/components/interfaces/SignIn/SignInWithExternalProvider', () => ({
+ SignInWithExternalProvider: ({ provider }: { provider: ExternalIdentityProviderConfig }) => (
+ Continue with {provider.displayName}
+ ),
+}))
+
+vi.mock('@/components/interfaces/SignIn/SignUpForm', () => ({
+ SignUpForm: ({ onSuccess }: { onSuccess?: () => void }) => {
+ const [isSubmitted, setIsSubmitted] = useState(false)
+
+ return (
+ <>
+ {
+ setIsSubmitted(true)
+ onSuccess?.()
+ }}
+ >
+ Complete email sign-up
+
+ {isSubmitted && Check your email
}
+ >
+ )
+ },
+}))
+
+vi.mock('@/hooks/misc/useEnabledIdentityProviders', () => ({
+ useEnabledIdentityProviders: () => [GITHUB_IDENTITY_PROVIDER, CHATGPT_IDENTITY_PROVIDER],
+}))
+
+vi.mock('@/hooks/misc/useInboundBranding', () => ({
+ useInboundBranding: () => ({ focusProvider: mocks.focusProvider }),
+}))
+
+vi.mock('@/hooks/misc/useIsFeatureEnabled', () => ({
+ useIsFeatureEnabled: () => ({
+ dashboardAuthSignUp: true,
+ dashboardAuthSignInWithSso: false,
+ dashboardAuthSignInWithEmail: true,
+ }),
+}))
+
+describe('SignUpPage', () => {
+ beforeEach(() => {
+ mocks.focusProvider = undefined
+ })
+
+ test('hides social sign-up options after email sign-up succeeds', async () => {
+ const user = userEvent.setup()
+ customRender( )
+
+ expect(screen.getByRole('button', { name: 'Continue with GitHub' })).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'Continue with ChatGPT' })).toBeInTheDocument()
+ expect(screen.getByText('or')).toBeInTheDocument()
+ expect(screen.queryByText('Check your email')).not.toBeInTheDocument()
+
+ await user.click(screen.getByRole('button', { name: 'Complete email sign-up' }))
+
+ expect(screen.queryByRole('button', { name: 'Continue with GitHub' })).not.toBeInTheDocument()
+ expect(screen.queryByRole('button', { name: 'Continue with ChatGPT' })).not.toBeInTheDocument()
+ expect(screen.queryByText('or')).not.toBeInTheDocument()
+ expect(screen.getByText('Check your email')).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: 'Sign in' })).toHaveAttribute('href', '/sign-in')
+ })
+
+ test('hides the focused provider after email sign-up succeeds', async () => {
+ const user = userEvent.setup()
+ mocks.focusProvider = GITHUB_IDENTITY_PROVIDER
+ customRender( )
+
+ expect(screen.getByRole('button', { name: 'Continue with GitHub' })).toBeInTheDocument()
+ expect(screen.queryByText('Check your email')).not.toBeInTheDocument()
+
+ await user.click(screen.getByRole('button', { name: 'Show other options' }))
+ await user.click(screen.getByRole('button', { name: 'Complete email sign-up' }))
+
+ expect(screen.queryByRole('button', { name: 'Continue with GitHub' })).not.toBeInTheDocument()
+ expect(screen.queryByRole('button', { name: 'Continue with ChatGPT' })).not.toBeInTheDocument()
+ expect(screen.queryByText('or')).not.toBeInTheDocument()
+ expect(screen.getByText('Check your email')).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: 'Sign in' })).toHaveAttribute('href', '/sign-in')
+ })
+})
diff --git a/apps/www/app/(home)/page.tsx b/apps/www/app/(home)/page.tsx
index 26e4afba5940a..8aa34bd438c2f 100644
--- a/apps/www/app/(home)/page.tsx
+++ b/apps/www/app/(home)/page.tsx
@@ -2,12 +2,28 @@ import type { Metadata } from 'next'
import { FrameworksSection } from './_components/FrameworksSection'
import { HomeContent } from './_components/HomeContent'
+import { organizationSchema, serializeJsonLd, websiteSchema } from '@/lib/json-ld'
import { mdAlternates } from '@/lib/md-alternates'
export const metadata: Metadata = {
- alternates: mdAlternates('index'),
+ alternates: {
+ ...mdAlternates('index'),
+ canonical: 'https://supabase.com',
+ },
}
export default function HomePage() {
- return } />
+ return (
+ <>
+
+
+ } />
+ >
+ )
}
diff --git a/apps/www/components/Error404.tsx b/apps/www/components/Error404.tsx
index f2e6e5c3a4142..5032d9d7eda0a 100644
--- a/apps/www/components/Error404.tsx
+++ b/apps/www/components/Error404.tsx
@@ -30,17 +30,17 @@ const Error404 = () => {