Files
6236ee9ef9 POC: bring back MSW to remove the pattern of vi.mock (#46439)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Right now our tests for API mocking is using vi.mock and mocking that
query or fetch handler. This is not the right approach IMO, 2 years ago
@jordienr added MSW with some very powerful helpers. The idea is to move
component test that rely on API using MSW within ViteTest. Principles
are simple:
- Mock API responses
- Mount your component that uses API responses
- Tests and assert on UI 
- Added Skill for Clanker

This pattern is 100 times better than what we have

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Tests**
* Expanded and strengthened test suites for secrets, org lookup, support
flows, OAuth auth, and onboarding; mocks now use contract-backed
responses for more realistic coverage.

* **Documentation**
* Added a comprehensive guide describing a standardized pattern for
component tests that mock network requests.

* **Chores**
* Improved test helpers, typing for API mocks, and test runner
configuration for more reliable and maintainable tests.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46439?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <alaister@users.noreply.github.com>
Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-05-28 12:58:50 +00:00

149 lines
4.7 KiB
TypeScript

import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { platformComponents as components } from 'api-types'
import { FeatureFlagContext } from 'common'
import { HttpResponse } from 'msw'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import { RedeemCreditsScreen } from './RedeemCredits'
import type { ProfileContextType } from '@/lib/profile'
import { createMockOrganizationResponse } from '@/tests/helpers'
import { customRender } from '@/tests/lib/custom-render'
import { addAPIMock } from '@/tests/lib/msw'
import { routerMock } from '@/tests/lib/route-mock'
type OrganizationResponse = components['schemas']['OrganizationResponse']
const { creditRedemptionProps } = vi.hoisted(() => ({
creditRedemptionProps: vi.fn(),
}))
const { creditRedemptionQueryCode } = vi.hoisted(() => ({
creditRedemptionQueryCode: { current: undefined as string | undefined },
}))
vi.mock('@/components/interfaces/Organization/BillingSettings/CreditCodeRedemption', () => {
return {
CreditCodeRedemption: (props: { slug?: string }) => {
creditRedemptionProps({ ...props, queryCode: creditRedemptionQueryCode.current })
return (
<div data-testid="credit-redemption">
Credit redemption for {props.slug} with code {creditRedemptionQueryCode.current}
</div>
)
},
}
})
const DEFAULT_PROFILE_CONTEXT: ProfileContextType = {
profile: {
id: 1,
auth0_id: 'auth0|test',
gotrue_id: 'gotrue-test',
username: 'testuser',
primary_email: 'test@example.com',
first_name: null,
last_name: null,
mobile: null,
is_alpha_user: false,
is_sso_user: false,
disabled_features: [],
free_project_limit: null,
},
error: null,
isLoading: false,
isError: false,
isSuccess: true,
}
const ORGANIZATION = createMockOrganizationResponse({
id: 1,
name: 'Acme Production',
slug: 'acme-production',
plan: { id: 'pro', name: 'Pro' },
})
function renderScreen() {
return customRender(
<FeatureFlagContext.Provider value={{ configcat: {}, posthog: {}, hasLoaded: true }}>
<RedeemCreditsScreen />
</FeatureFlagContext.Provider>,
{ profileContext: DEFAULT_PROFILE_CONTEXT }
)
}
describe('RedeemCreditsScreen', () => {
beforeEach(() => {
vi.clearAllMocks()
creditRedemptionQueryCode.current = undefined
routerMock.setCurrentUrl('/redeem')
})
test('renders ready state from organizations query and opens redemption for selected organization', async () => {
const user = userEvent.setup()
routerMock.setCurrentUrl('/redeem?code=SUPA-CREDIT-123')
creditRedemptionQueryCode.current = 'SUPA-CREDIT-123'
addAPIMock({
method: 'get',
path: '/platform/organizations',
response: () => HttpResponse.json<OrganizationResponse[]>([ORGANIZATION]),
})
renderScreen()
await user.click(await screen.findByRole('button', { name: /Acme Production/ }))
await user.click(screen.getByRole('button', { name: 'Redeem credits' }))
expect(await screen.findByTestId('credit-redemption')).toHaveTextContent(
'Credit redemption for acme-production with code SUPA-CREDIT-123'
)
expect(creditRedemptionProps).toHaveBeenCalledWith(
expect.objectContaining({
slug: 'acme-production',
queryCode: 'SUPA-CREDIT-123',
})
)
})
test('routes new organization creation back to the current redeem URL', async () => {
routerMock.setCurrentUrl('/redeem?code=SUPA-CREDIT-123')
addAPIMock({
method: 'get',
path: '/platform/organizations',
response: () => HttpResponse.json<OrganizationResponse[]>([ORGANIZATION]),
})
renderScreen()
const createOrganizationLink = await screen.findByRole('link', {
name: /Create new organization/,
})
expect(createOrganizationLink).toHaveAttribute(
'href',
'/new?returnTo=%2Fredeem%3Fcode%3DSUPA-CREDIT-123&returnToOrgParam=selected_org'
)
})
test('preselects an organization returned from new organization creation', async () => {
const user = userEvent.setup()
routerMock.setCurrentUrl('/redeem?code=SUPA-CREDIT-123&selected_org=acme-production')
creditRedemptionQueryCode.current = 'SUPA-CREDIT-123'
addAPIMock({
method: 'get',
path: '/platform/organizations',
response: () => HttpResponse.json<OrganizationResponse[]>([ORGANIZATION]),
})
renderScreen()
const redeemButton = await screen.findByRole('button', { name: 'Redeem credits' })
await waitFor(() => expect(redeemButton).toBeEnabled())
await user.click(redeemButton)
expect(await screen.findByTestId('credit-redemption')).toHaveTextContent(
'Credit redemption for acme-production with code SUPA-CREDIT-123'
)
})
})