feat(rennmaeuse): Tiere-Liste neu im Rennmaus-Filter-Design
Filter-UX aus dem Design: - Quick-Chip-Kategorien (Status/Geschlecht/Farbschlag/Herkunft) öffnen ein Bottom-Sheet mit Mehrfachauswahl (suchbar bei Farbschlag/Herkunft); aktive Filter als entfernbare Chips; Sortierung + "Externe Ahnen" als gleich große Chips (Sortierung öffnet dasselbe Drawer). - Neue zweizeilige Tierkarte (Name + Geschlechts-Kreis + Status-Badge, Meta-Zeile), cardIn/Chip/Sheet-Animationen, Titel blendet beim Scrollen aus. App-weit: - Design-Palette (Creme/Braun/Terrakotta), Hanken-Grotesk-Schrift und gemeinsamer --shadow-Token; obere "Rennmaus-Manager"-Titelleiste entfernt (mobil bleibt die untere Aktionsleiste); fixe Shell-Höhe, nur der Hauptbereich scrollt. Funktion: - Infinite Scroll via useInfiniteList (seitenweise) + IntersectionObserver-Sentinel. - Namensgenerator: Option männlich/weiblich/egal. - Mehrfach-OR-Filter über gridify orGroup; e2e an die neue UI angepasst. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/** BESTAND-FILTER: die Tiere-Liste zeigt standardmäßig nur den eigenen Bestand. */
|
||||
import { de, expect, gotoSection, openFilterPanel, skipUnlessMock, test } from './fixtures'
|
||||
import { de, expect, gotoSection, skipUnlessMock, test } from './fixtures'
|
||||
|
||||
const t = de.pages.gerbils
|
||||
|
||||
@@ -19,9 +19,8 @@ test('Toggle „Externe Ahnen einblenden” zeigt externe Tiere mit Extern-Marki
|
||||
await gotoSection(page, de.nav.gerbils)
|
||||
await expect(page.locator('.gerbil-row', { hasText: 'Krümel' })).toBeVisible()
|
||||
|
||||
// UX-MOBILE-1: Checkbox liegt im Filter-Drawer — auf Mobil erst öffnen.
|
||||
await openFilterPanel(page)
|
||||
await page.getByRole('checkbox', { name: t.filters.showExternal }).check()
|
||||
// DESIGN (Rennmaus Filter): „Externe Ahnen" ist ein Toggle-Chip (Button), kein Checkbox.
|
||||
await page.getByRole('button', { name: t.filters.showExternalShort }).click()
|
||||
|
||||
const maxRow = page.locator('.gerbil-row', { hasText: 'Max' })
|
||||
await expect(maxRow).toBeVisible()
|
||||
|
||||
@@ -1,85 +1,54 @@
|
||||
/**
|
||||
* UX-MOBILE-1: FilterPanel — einklappbare Filter auf Smartphone, inline auf Desktop.
|
||||
* DESIGN (Rennmaus Filter.html): Quick-Chips-Filter der Rennmäuse-Liste.
|
||||
*
|
||||
* Telefon (390px): Filter-Button sichtbar, Drawer eingeklappt; Tippen öffnet/schliesst.
|
||||
* Desktop (1280px): Alle Controls direkt sichtbar, kein Toggle-Button.
|
||||
* Eine Pillen-Reihe (Status / Geschlecht / Farbschlag / Herkunft) öffnet je ein
|
||||
* Bottom-Sheet mit Mehrfachauswahl; gewählte Werte erscheinen als entfernbare
|
||||
* Chips, „Alle löschen" setzt zurück. Mobile-first; identisches Verhalten auf Desktop.
|
||||
*/
|
||||
import { de, expect, gotoSection, skipUnlessMock, test } from './fixtures'
|
||||
import { de, expect, gotoSection, selectListFilter, skipUnlessMock, test } from './fixtures'
|
||||
|
||||
const t = de.pages.gerbils
|
||||
|
||||
test.describe('FilterPanel – Rennmäuse-Liste', () => {
|
||||
test('Phone: Filter-Drawer standardmäßig eingeklappt, Toggle-Button sichtbar', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.describe('Quick-Chips-Filter – Rennmäuse-Liste', () => {
|
||||
test('Kategorie-Pillen sind sichtbar', async ({ page }) => {
|
||||
skipUnlessMock()
|
||||
if (testInfo.project.name !== 'phone') return
|
||||
|
||||
await gotoSection(page, de.nav.gerbils)
|
||||
await expect(page.getByRole('heading', { name: t.title, exact: true })).toBeVisible()
|
||||
|
||||
// Toggle-Button sichtbar.
|
||||
const toggle = page.locator('.filter-panel__toggle')
|
||||
await expect(toggle).toBeVisible()
|
||||
|
||||
// Status-Beschriftung im Drawer ist noch verborgen.
|
||||
await expect(page.getByText(t.filters.status, { exact: true }).first()).not.toBeVisible()
|
||||
const quickrow = page.locator('.rmf-quickrow')
|
||||
await expect(quickrow.getByRole('button', { name: t.filters.status })).toBeVisible()
|
||||
await expect(quickrow.getByRole('button', { name: t.filters.gender })).toBeVisible()
|
||||
await expect(quickrow.getByRole('button', { name: t.filters.colorVariety })).toBeVisible()
|
||||
await expect(quickrow.getByRole('button', { name: t.fields.origin })).toBeVisible()
|
||||
})
|
||||
|
||||
test('Phone: Toggle öffnet und schließt den Filter-Drawer', async ({
|
||||
test('Mehrfachauswahl erzeugt entfernbare Chips, „Alle löschen" setzt zurück', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
}) => {
|
||||
skipUnlessMock()
|
||||
if (testInfo.project.name !== 'phone') return
|
||||
|
||||
await gotoSection(page, de.nav.gerbils)
|
||||
const toggle = page.locator('.filter-panel__toggle')
|
||||
|
||||
// Öffnen → Status-Feld wird sichtbar.
|
||||
await toggle.click()
|
||||
await expect(page.getByText(t.filters.status, { exact: true }).first()).toBeVisible()
|
||||
// Default: Zucht-Chip ist aktiv.
|
||||
await expect(page.locator('.rmf-chip-val', { hasText: t.statusLabels.Breeding })).toBeVisible()
|
||||
|
||||
// Schließen → wieder verborgen.
|
||||
await toggle.click()
|
||||
await expect(page.getByText(t.filters.status, { exact: true }).first()).not.toBeVisible()
|
||||
// Geschlecht = männlich über das Sheet wählen → zweiter Chip erscheint.
|
||||
await selectListFilter(page, t.filters.gender, t.genderLabels.male)
|
||||
await expect(page.locator('.rmf-chip-val', { hasText: t.genderLabels.male })).toBeVisible()
|
||||
|
||||
// „Alle löschen" entfernt sämtliche Chips.
|
||||
await page.getByRole('button', { name: de.filterBar.clearAll }).click()
|
||||
await expect(page.locator('.rmf-chip')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('Phone: Badge zählt aktive Filter korrekt', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test('Einzelner Chip lässt sich per Klick entfernen', async ({ page }) => {
|
||||
skipUnlessMock()
|
||||
if (testInfo.project.name !== 'phone') return
|
||||
|
||||
await gotoSection(page, de.nav.gerbils)
|
||||
const toggle = page.locator('.filter-panel__toggle')
|
||||
|
||||
// Initial: Status='Active' ist Default → Badge zeigt kein „(N)".
|
||||
await expect(toggle).toHaveText('Filter')
|
||||
|
||||
// Filter-Drawer öffnen und Geschlecht setzen → 1 aktiver Filter.
|
||||
await toggle.click()
|
||||
await page.locator('.filter-panel__drawer select').nth(1).selectOption('male')
|
||||
await expect(toggle).toHaveText('Filter (1)')
|
||||
|
||||
// Zurücksetzen → Badge weg.
|
||||
await page.getByRole('button', { name: de.filterPanel.resetButton }).click()
|
||||
await expect(toggle).toHaveText('Filter')
|
||||
})
|
||||
|
||||
test('Desktop: Toggle-Button nicht sichtbar, alle Controls inline', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
skipUnlessMock()
|
||||
if (testInfo.project.name !== 'desktop') return
|
||||
|
||||
await gotoSection(page, de.nav.gerbils)
|
||||
await expect(page.getByRole('heading', { name: t.title, exact: true })).toBeVisible()
|
||||
|
||||
// Kein Toggle-Button auf Desktop (display:none via Media Query).
|
||||
const toggle = page.locator('.filter-panel__toggle')
|
||||
await expect(toggle).toBeHidden()
|
||||
|
||||
// Status-Beschriftung direkt sichtbar.
|
||||
await expect(page.getByText(t.filters.status, { exact: true }).first()).toBeVisible()
|
||||
const breedingChip = page.locator('.rmf-chip', {
|
||||
has: page.locator('.rmf-chip-val', { hasText: t.statusLabels.Breeding }),
|
||||
})
|
||||
await expect(breedingChip).toBeVisible()
|
||||
await breedingChip.click()
|
||||
await expect(breedingChip).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -86,6 +86,18 @@ export async function openFilterPanel(page: Page) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DESIGN (Rennmaus Filter): wählt einen Wert im Quick-Chips-Filter der Rennmäuse-Liste.
|
||||
* Öffnet das Bottom-Sheet der Kategorie, hakt die Option an und schließt mit „Fertig".
|
||||
*/
|
||||
export async function selectListFilter(page: Page, categoryLabel: string, optionLabel: string) {
|
||||
// Pille der Kategorie in der Quick-Chips-Reihe öffnen (nicht den aktiven Chip treffen).
|
||||
await page.locator('.rmf-quickrow').getByRole('button', { name: categoryLabel }).click()
|
||||
const sheet = page.getByRole('dialog', { name: categoryLabel })
|
||||
await sheet.getByRole('button', { name: optionLabel, exact: true }).click()
|
||||
await sheet.getByRole('button', { name: de.filterBar.done }).click()
|
||||
}
|
||||
|
||||
/** Eindeutiger Name für LIVE-taugliche Create-Flows. */
|
||||
export const uniqueName = (prefix: string) =>
|
||||
`${prefix} E2E ${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`
|
||||
|
||||
@@ -3,7 +3,7 @@ import { de, expect, gotoSection, test } from './fixtures'
|
||||
|
||||
test('alle Bereiche sind über die Navigation erreichbar', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await expect(page.getByRole('heading', { name: de.app.title })).toBeVisible()
|
||||
// DESIGN: the top "Rennmaus-Manager" title bar was removed — the page heading remains.
|
||||
await expect(page.getByRole('heading', { name: de.pages.gerbils.title, exact: true })).toBeVisible()
|
||||
|
||||
await gotoSection(page, de.nav.gerbils)
|
||||
|
||||
@@ -37,10 +37,8 @@ test('Tier-Formular zeigt abgeleiteten Status Verstorben als Text + Hinweis', as
|
||||
test('Tiere-Liste zeigt Zucht als Standard-Status-Filter', async ({ page }) => {
|
||||
skipUnlessMock()
|
||||
await page.goto('/rennmaeuse')
|
||||
const statusSelect = page
|
||||
.locator('label.field', { has: page.locator(`span:text-is("${t.filters.status}")`) })
|
||||
.locator('select')
|
||||
await expect(statusSelect).toHaveValue('Breeding')
|
||||
// DESIGN (Rennmaus Filter): der Default-Status erscheint als aktiver, entfernbarer Chip.
|
||||
await expect(page.locator('.rmf-chip-val', { hasText: t.statusLabels.Breeding })).toBeVisible()
|
||||
})
|
||||
|
||||
test('Neues Tier anlegen mit Status Liebhaber', async ({ page }) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** QA-1: Tiere (Rennmäuse) — Liste/Suche, CRUD, Detail-Tabs (FEAT-1 + FEAT-6). */
|
||||
import { de, expect, gotoSection, openFilterPanel, skipUnlessMock, test, uniqueName } from './fixtures'
|
||||
import { de, expect, gotoSection, selectListFilter, skipUnlessMock, test, uniqueName } from './fixtures'
|
||||
|
||||
const t = de.pages.gerbils
|
||||
const tabs = de.pages.tierTabs
|
||||
@@ -23,13 +23,9 @@ test('Herkunft-Filter (originBreeder) zeigt nur Tiere der gewählten Zucht (SEAR
|
||||
await expect(page.getByRole('link', { name: /Krümel/ })).toBeVisible()
|
||||
await expect(page.getByRole('link', { name: /Fridolin/ })).toBeVisible()
|
||||
|
||||
// UX-MOBILE-1: Herkunft-Select liegt im Filter-Drawer — auf Mobil erst öffnen.
|
||||
await openFilterPanel(page)
|
||||
// Herkunft (originBreeder) auf die Seed-Zucht 'Clan-Kleine-Chaoten' (nur Krümel).
|
||||
await page
|
||||
.locator('label.field', { has: page.locator(`span:text-is("${t.fields.origin}")`) })
|
||||
.locator('select')
|
||||
.selectOption({ label: 'Clan-Kleine-Chaoten' })
|
||||
// DESIGN (Rennmaus Filter): Herkunft (originBreeder) über das Quick-Chips-Sheet
|
||||
// auf die Seed-Zucht 'Clan-Kleine-Chaoten' (nur Krümel) filtern.
|
||||
await selectListFilter(page, t.fields.origin, 'Clan-Kleine-Chaoten')
|
||||
|
||||
await expect(page.getByRole('link', { name: /Krümel/ })).toBeVisible()
|
||||
await expect(page.getByRole('link', { name: /Fridolin/ })).toBeHidden()
|
||||
|
||||
Reference in New Issue
Block a user