Refs #454 ## Automated implementation (requires human review) [SidebarComponent.tsx](frontend/src/components/layout/SidebarComponent.tsx) の1行を修正しました。`overflow-y-auto` を `md` 以上に限定し、スマホでサゼストがサイドバーの領域外で切られる原因を取り除きました。 差分を確認済みです。指定に従い、テスト・lint・ビルドは未実行です。実画面でのスマホ表示とPC表示の確認も未実施です。 ## Automated review (not a guarantee of correctness) ```text The change restricts sidebar scrolling to medium and larger viewports. No actionable regression was identified in the current diff. ``` ## Verification - Tests/builds/migrations were not authorized and were not invoked by the controller. - Codex was instructed not to run them; its command transcript should be inspected. - Do not merge before manual acceptance and required checks. --------- Co-authored-by: Codex Night Worker <codex-night@localhost> Reviewed-on: #456 Co-authored-by: テツノニジカ <969+codex-bot@noreply.localhost>
136 行
4.2 KiB
TypeScript
136 行
4.2 KiB
TypeScript
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
|
import { HelmetProvider } from 'react-helmet-async'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import SidebarComponent from '@/components/layout/SidebarComponent'
|
|
import { renderWithProviders as render } from '@/test/render'
|
|
|
|
|
|
const sidebarWidth = (element: Element | null): string =>
|
|
(element as HTMLElement).style.getPropertyValue ('--sidebar-width')
|
|
|
|
|
|
const sidebarRoot = (container: HTMLElement): Element =>
|
|
container.querySelector ('[style*="--sidebar-width"]')!
|
|
|
|
|
|
const sidebarTree = (props: Partial<Parameters<typeof SidebarComponent>[0]>) => (
|
|
<HelmetProvider>
|
|
<div data-sidebar-container>
|
|
<SidebarComponent sidebarKey="test-sidebar" {...props}>
|
|
Sidebar body
|
|
</SidebarComponent>
|
|
</div>
|
|
</HelmetProvider>)
|
|
|
|
|
|
const renderSidebar = (
|
|
props: Partial<Parameters<typeof SidebarComponent>[0]> = {},
|
|
containerWidth = 800,
|
|
) => {
|
|
Object.defineProperty (HTMLElement.prototype, 'clientWidth', {
|
|
configurable: true,
|
|
get () {
|
|
return this.hasAttribute ('data-sidebar-container') ? containerWidth : 0
|
|
},
|
|
})
|
|
|
|
const rendered = render (sidebarTree (props))
|
|
|
|
return rendered
|
|
}
|
|
|
|
|
|
describe ('SidebarComponent', () => {
|
|
beforeEach (() => {
|
|
Object.defineProperty (window, 'innerWidth', {
|
|
configurable: true,
|
|
value: 1_000,
|
|
})
|
|
|
|
Object.defineProperty (HTMLElement.prototype, 'setPointerCapture', {
|
|
configurable: true,
|
|
value: vi.fn (),
|
|
})
|
|
Object.defineProperty (HTMLElement.prototype, 'releasePointerCapture', {
|
|
configurable: true,
|
|
value: vi.fn (),
|
|
})
|
|
Object.defineProperty (HTMLElement.prototype, 'hasPointerCapture', {
|
|
configurable: true,
|
|
value: vi.fn (() => true),
|
|
})
|
|
|
|
vi.spyOn (HTMLElement.prototype, 'setPointerCapture').mockImplementation (() => {
|
|
;
|
|
})
|
|
vi.spyOn (HTMLElement.prototype, 'releasePointerCapture').mockImplementation (() => {
|
|
;
|
|
})
|
|
vi.spyOn (HTMLElement.prototype, 'hasPointerCapture').mockReturnValue (true)
|
|
})
|
|
|
|
it ('limits sidebar scrolling to desktop to avoid clipping mobile suggestions', () => {
|
|
renderSidebar ()
|
|
|
|
// jsdom has no layout; guard the responsive classes responsible for clipping.
|
|
const sidebar = screen.getByText ('Sidebar body')
|
|
|
|
expect (sidebar).not.toHaveClass ('overflow-y-auto')
|
|
expect (sidebar).toHaveClass ('md:overflow-y-auto')
|
|
})
|
|
|
|
it ('uses maxWidth without applying the container main-width reserve twice', async () => {
|
|
localStorage.setItem ('sidebar:test-sidebar:left:width', '700')
|
|
const onWidthChange = vi.fn ()
|
|
|
|
const { container } = renderSidebar ({ maxWidth: 700, onWidthChange }, 800)
|
|
|
|
await waitFor (() => {
|
|
expect (sidebarWidth (sidebarRoot (container))).toBe ('700px')
|
|
})
|
|
expect (onWidthChange).toHaveBeenLastCalledWith (700)
|
|
})
|
|
|
|
it ('resets width and removes only its storage key on double click', async () => {
|
|
localStorage.setItem ('sidebar:test-sidebar:right:width', '420')
|
|
localStorage.setItem ('sidebar:other-sidebar:right:width', '480')
|
|
|
|
const { container } = renderSidebar ({ side: 'right' }, 900)
|
|
|
|
await waitFor (() => {
|
|
expect (sidebarWidth (sidebarRoot (container))).toBe ('420px')
|
|
})
|
|
|
|
fireEvent.doubleClick (screen.getByRole ('separator', { name: 'サイドバー幅' }))
|
|
|
|
await waitFor (() => {
|
|
expect (sidebarWidth (sidebarRoot (container))).toBe ('256px')
|
|
})
|
|
expect (localStorage.getItem ('sidebar:test-sidebar:right:width')).toBeNull ()
|
|
expect (localStorage.getItem ('sidebar:other-sidebar:right:width')).toBe ('480')
|
|
})
|
|
|
|
it ('clamps current width on maxWidth changes without re-reading storage', async () => {
|
|
localStorage.setItem ('sidebar:test-sidebar:left:width', '420')
|
|
|
|
const { container, rerender } = renderSidebar ({ maxWidth: 420 }, 900)
|
|
|
|
await waitFor (() => {
|
|
expect (sidebarWidth (sidebarRoot (container))).toBe ('420px')
|
|
})
|
|
|
|
rerender (sidebarTree ({ maxWidth: 300 }))
|
|
|
|
await waitFor (() => {
|
|
expect (sidebarWidth (sidebarRoot (container))).toBe ('300px')
|
|
})
|
|
|
|
rerender (sidebarTree ({ maxWidth: 500 }))
|
|
|
|
await waitFor (() => {
|
|
expect (sidebarWidth (sidebarRoot (container))).toBe ('300px')
|
|
})
|
|
})
|
|
})
|