diff --git a/apps/desktop/src/plugins/hermes-bots/pet.test.tsx b/apps/desktop/src/plugins/hermes-bots/pet.test.tsx index 6e913cb28b..a3d08e8728 100644 --- a/apps/desktop/src/plugins/hermes-bots/pet.test.tsx +++ b/apps/desktop/src/plugins/hermes-bots/pet.test.tsx @@ -12,7 +12,7 @@ * failure must be evicted; a success must not be refetched. */ -import { render, waitFor } from '@testing-library/react' +import { fireEvent, render, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { hostMock, UnboundedCache, useQueryMock } = vi.hoisted(() => ({ @@ -81,6 +81,55 @@ afterEach(() => { vi.restoreAllMocks() }) +describe('the pet gallery', () => { + it('reserves paint space around boundary tiles inside the bounded scroller', async () => { + stubFetch(async () => ({ blob: async () => new Blob() })) + const PetTab = await loadPetTab() + const view = render() + await waitFor(() => expect(view.container.querySelector('img')).toBeTruthy()) + const tile = view.getByText('Axolotl').closest('button')! + const scroller = tile.parentElement!.parentElement! + + // The selection ring paints one pixel outside each tile. The scrollport + // must leave room for it even at the first/last row and outer columns. + const style = getComputedStyle(scroller) + + for (const side of ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'] as const) { + expect(parseFloat(style[side]) || 0).toBeGreaterThanOrEqual(1) + } + + expect(parseFloat(style.maxHeight)).toBeGreaterThan(0) + view.unmount() + }) + + it('keeps selection while scrolling for more and resets the search window', async () => { + useQueryMock.mockReturnValue({ data: { pets: Array.from({ length: 60 }, (_, i) => ({ + displayName: `Pet ${i}`, slug: `pet-${i}`, spritesheetUrl: SHEET + })) } }) + stubFetch(async () => ({ blob: async () => new Blob() })) + const PetTab = await loadPetTab() + const onImage = vi.fn() + const view = render() + const first = view.getByText('Pet 0').closest('button')! + fireEvent.click(first) + await waitFor(() => expect(onImage).toHaveBeenCalledWith('data:image/png;base64,ok')) + const scroller = first.parentElement!.parentElement! + Object.defineProperties(scroller, { + clientHeight: { value: 220 }, scrollHeight: { value: 600 }, scrollTop: { value: 400 } + }) + fireEvent.scroll(scroller) + expect(view.getByText('Pet 47')).toBeTruthy() + expect(view.queryByText('Pet 48')).toBeNull() + expect(first.className).toContain('ring-1') + fireEvent.change(view.getByRole('textbox'), { target: { value: 'Pet 59' } }) + expect(view.getByText('Pet 59')).toBeTruthy() + fireEvent.change(view.getByRole('textbox'), { target: { value: '' } }) + expect(view.queryByText('Pet 24')).toBeNull() + expect(onImage).toHaveBeenCalledTimes(1) + view.unmount() + }) +}) + describe('the sprite-frame cache', () => { it('never leaves a failed fetch parked in the cache', async () => { stubFetch(async () => { diff --git a/apps/desktop/src/plugins/hermes-bots/pet.tsx b/apps/desktop/src/plugins/hermes-bots/pet.tsx index 700e1b98f4..b14fded278 100644 --- a/apps/desktop/src/plugins/hermes-bots/pet.tsx +++ b/apps/desktop/src/plugins/hermes-bots/pet.tsx @@ -230,7 +230,9 @@ export function PetTab({ image, onImage }: PetTabProps) { className="overflow-y-auto" onScroll={onScroll} style={{ - maxHeight: 220 + // Leave room for the selection ring outside boundary tiles. + maxHeight: 220, + padding: 2 }} >