refactor
This commit is contained in:
parent
2967f1a5af
commit
618b700feb
6 changed files with 738 additions and 0 deletions
166
web/src/components/fallback-switcher.integration.test.tsx
Normal file
166
web/src/components/fallback-switcher.integration.test.tsx
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
/**
|
||||
* Fallback switcher integration test.
|
||||
*
|
||||
* If the shipped controls never upgrade — a blocked module load, an older bundle, a
|
||||
* proxy hiccup — the admin falls back to `LegacyThemeSwitcher`. This test keeps that
|
||||
* path genuinely usable: the theme dropdown must always be present (even in the admin
|
||||
* chrome), and choosing a theme or toggling light/dark must apply the document
|
||||
* attributes the preview stylesheet is scoped to and persist the choice.
|
||||
*
|
||||
* It renders the REAL `ThemeSwitcher` under the REAL `AdminThemeProvider` with
|
||||
* `loadRuntimeControls` mocked to never resolve, so `controlsReady` stays false and the
|
||||
* fallback is what renders.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
import { AdminThemeProvider } from './admin-theme'
|
||||
import { ThemeSwitcher } from './shell'
|
||||
|
||||
vi.mock('../lib/runtime-controls', () => ({
|
||||
// Never resolves: the shipped controls never upgrade, so the fallback stays in place.
|
||||
loadRuntimeControls: () => new Promise<void>(() => undefined),
|
||||
}))
|
||||
|
||||
vi.mock('../lib/api', () => ({
|
||||
getManifest: vi.fn(() => Promise.resolve({ apiVersion: '1', baseUrl: '/rt/v1', themes: [] })),
|
||||
getSession: vi.fn(() => Promise.resolve({ authenticated: true, mode: 'dev' })),
|
||||
updatePreferences: vi.fn(() => Promise.resolve({ selectedTheme: '', selectedAppearance: '' })),
|
||||
resetPreferences: vi.fn(() => Promise.resolve({ selectedTheme: '', selectedAppearance: '' })),
|
||||
}))
|
||||
|
||||
const api = await import('../lib/api')
|
||||
|
||||
const THEME_ATTR = 'data-bananas-theme'
|
||||
const APPEARANCE_ATTR = 'data-bananas-appearance'
|
||||
|
||||
function renderSwitcher() {
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } })
|
||||
return render(
|
||||
<QueryClientProvider client={client}>
|
||||
<AdminThemeProvider>
|
||||
<ThemeSwitcher />
|
||||
</AdminThemeProvider>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
// The dropdown is populated from the runtime manifest, which arrives asynchronously. A
|
||||
// controlled <select> ignores a value that has no matching <option> yet, so the tests
|
||||
// must wait for the themes to be listed before choosing one — the same as a human.
|
||||
async function previewSelect() {
|
||||
const select = await screen.findByLabelText('Preview theme')
|
||||
await waitFor(() => expect(select).toHaveTextContent('daylight'))
|
||||
await waitFor(() => expect(select).toHaveTextContent('midnight'))
|
||||
return select
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear()
|
||||
vi.mocked(api.getManifest).mockResolvedValue({
|
||||
apiVersion: '1',
|
||||
baseUrl: '/rt/v1',
|
||||
themes: [
|
||||
{
|
||||
slug: 'midnight',
|
||||
name: 'Midnight',
|
||||
defaultAppearance: 'dark',
|
||||
version: 1,
|
||||
css: '',
|
||||
tokens: '',
|
||||
},
|
||||
{
|
||||
slug: 'daylight',
|
||||
name: 'Daylight',
|
||||
defaultAppearance: 'light',
|
||||
version: 1,
|
||||
css: '',
|
||||
tokens: '',
|
||||
},
|
||||
],
|
||||
})
|
||||
vi.mocked(api.getSession).mockResolvedValue({ authenticated: true, mode: 'dev' })
|
||||
document.documentElement.removeAttribute(THEME_ATTR)
|
||||
document.documentElement.removeAttribute(APPEARANCE_ATTR)
|
||||
document.querySelector('link[data-bananas-admin]')?.remove()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('fallback switcher (shipped controls never loaded)', () => {
|
||||
it('renders the theme dropdown even while in the admin chrome', async () => {
|
||||
renderSwitcher()
|
||||
const select = await previewSelect()
|
||||
// Both published themes are offered regardless of the current mode.
|
||||
expect(select).toHaveTextContent(/daylight/i)
|
||||
expect(select).toHaveTextContent(/midnight/i)
|
||||
})
|
||||
|
||||
it('choosing a theme from the dropdown applies its default appearance', async () => {
|
||||
renderSwitcher()
|
||||
const select = await previewSelect()
|
||||
|
||||
fireEvent.change(select, { target: { value: 'daylight' } })
|
||||
|
||||
const root = document.documentElement
|
||||
// Provider is the sole writer in the fallback, so the preview attributes must land.
|
||||
await waitFor(() => expect(root.getAttribute(THEME_ATTR)).toBe('daylight'))
|
||||
expect(root.getAttribute(APPEARANCE_ATTR)).toBe('light')
|
||||
await waitFor(() =>
|
||||
expect(api.updatePreferences).toHaveBeenCalledWith({
|
||||
selectedTheme: 'daylight',
|
||||
selectedAppearance: 'light',
|
||||
}),
|
||||
)
|
||||
expect(
|
||||
document
|
||||
.querySelector('link[data-bananas-admin="bananas-admin-preview"]')
|
||||
?.getAttribute('href'),
|
||||
).toContain('slug=daylight')
|
||||
})
|
||||
|
||||
it('choosing a dark-default theme previews it dark', async () => {
|
||||
renderSwitcher()
|
||||
const select = await previewSelect()
|
||||
|
||||
fireEvent.change(select, { target: { value: 'midnight' } })
|
||||
|
||||
await waitFor(() => expect(document.documentElement.getAttribute(THEME_ATTR)).toBe('midnight'))
|
||||
expect(document.documentElement.getAttribute(APPEARANCE_ATTR)).toBe('dark')
|
||||
})
|
||||
|
||||
it('the light/dark buttons repaint and persist', async () => {
|
||||
renderSwitcher()
|
||||
// First pick a theme so we are in a preview mode with a concrete slug.
|
||||
const select = await previewSelect()
|
||||
fireEvent.change(select, { target: { value: 'daylight' } })
|
||||
await waitFor(() => expect(document.documentElement.getAttribute(THEME_ATTR)).toBe('daylight'))
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Dark' }))
|
||||
|
||||
await waitFor(() => expect(document.documentElement.getAttribute(APPEARANCE_ATTR)).toBe('dark'))
|
||||
await waitFor(() =>
|
||||
expect(api.updatePreferences).toHaveBeenCalledWith({
|
||||
selectedTheme: 'daylight',
|
||||
selectedAppearance: 'dark',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returning to admin clears the preview attributes', async () => {
|
||||
renderSwitcher()
|
||||
const select = await previewSelect()
|
||||
fireEvent.change(select, { target: { value: 'daylight' } })
|
||||
await waitFor(() => expect(document.documentElement.hasAttribute(THEME_ATTR)).toBe(true))
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Admin' }))
|
||||
|
||||
await waitFor(() => expect(document.documentElement.hasAttribute(THEME_ATTR)).toBe(false))
|
||||
await waitFor(() => expect(document.documentElement.hasAttribute(APPEARANCE_ATTR)).toBe(false))
|
||||
await waitFor(() => expect(api.resetPreferences).toHaveBeenCalled())
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue