feat(web): Favoriten-Widget — optionaler Titel (Kopfzeile, FavoritesConfig, i18n)
- Favoriten-Widget: config.title (nur typeof string) steuert eine Kopfzeile
im Notiz-Look; leer -> keine Kopfzeile; im Bearbeitungsmodus immer ein
Titelfeld (widgetNoDrag, 1500 ms entprellt, { title })
- Einstellungen -> Dashboard -> Widgets: FavoritesConfig (Muster NoteConfig)
mit uebersetzter Beschriftung, Instanz-Kopfzeile zeigt "— {title}" jetzt
fuer note UND favorites
- NoteConfig: hart kodiertes "Title" durch uebersetzten Schluessel ersetzt
- 3 neue Uebersetzungsschluessel (note.titleLabel, favorites.titleLabel,
favorites.titlePlaceholder) in de/en
- favorites-Tests 7+4, Panel-Tests 7+3, Umlaut-Waechter 3/3 gruen, tsc Exit 0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
updateFavorite,
|
||||
deleteFavorite,
|
||||
} from '@/lib/favorites-api';
|
||||
import { updateWidgetConfig } from '@/lib/dashboard-api';
|
||||
|
||||
// Must import AFTER mocks
|
||||
import { FavoritesWidget } from './favorites-widget';
|
||||
@@ -34,6 +35,7 @@ const mockFetch = fetchFavorites as ReturnType<typeof vi.fn>;
|
||||
const mockCreate = createFavorite as ReturnType<typeof vi.fn>;
|
||||
const mockUpdate = updateFavorite as ReturnType<typeof vi.fn>;
|
||||
const mockDelete = deleteFavorite as ReturnType<typeof vi.fn>;
|
||||
const mockUpdateWidgetConfig = updateWidgetConfig as ReturnType<typeof vi.fn>;
|
||||
|
||||
const BASE_FAVORITES = [
|
||||
{
|
||||
@@ -287,4 +289,129 @@ describe('FavoritesWidget', () => {
|
||||
// 'Notion' has iconUrl null — the letter fallback span should show 'N'
|
||||
expect(screen.getByTestId('letter-fallback-fav-id-2')).toHaveTextContent('N');
|
||||
});
|
||||
|
||||
// quick-260916-iex: optionaler Titel
|
||||
describe('optionaler Titel (quick-260916-iex)', () => {
|
||||
it('Ansicht ohne (leeren/ungueltigen) Titel zeigt keine Kopfzeile', async () => {
|
||||
const r1 = render(
|
||||
<FavoritesWidget instanceId="fav-1" config={{}} isEditMode={false} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('GitHub')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByRole('heading')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByPlaceholderText('favorites.titlePlaceholder'),
|
||||
).not.toBeInTheDocument();
|
||||
r1.unmount();
|
||||
|
||||
const r2 = render(
|
||||
<FavoritesWidget
|
||||
instanceId="fav-1"
|
||||
config={{ title: ' ' }}
|
||||
isEditMode={false}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('GitHub')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByRole('heading')).not.toBeInTheDocument();
|
||||
r2.unmount();
|
||||
|
||||
render(
|
||||
<FavoritesWidget
|
||||
instanceId="fav-1"
|
||||
config={{ title: 42 }}
|
||||
isEditMode={false}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('GitHub')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByRole('heading')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Ansicht mit Titel zeigt eine H2-Kopfzeile', async () => {
|
||||
render(
|
||||
<FavoritesWidget
|
||||
instanceId="fav-1"
|
||||
config={{ title: 'Werkzeuge' }}
|
||||
isEditMode={false}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('GitHub')).toBeInTheDocument();
|
||||
});
|
||||
const heading = screen.getByRole('heading', { name: 'Werkzeuge' });
|
||||
expect(heading.tagName).toBe('H2');
|
||||
expect(heading.className).toContain('text-sm');
|
||||
expect(heading.className).toContain('font-semibold');
|
||||
expect(
|
||||
screen.queryByPlaceholderText('favorites.titlePlaceholder'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Bearbeitungsmodus ohne Titel zeigt ein Titelfeld statt einer Kopfzeile', async () => {
|
||||
render(
|
||||
<FavoritesWidget instanceId="fav-1" config={{}} isEditMode={true} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('GitHub')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const titleField = screen.getByPlaceholderText(
|
||||
'favorites.titlePlaceholder',
|
||||
) as HTMLInputElement;
|
||||
expect(titleField.value).toBe('');
|
||||
expect(titleField.className).toContain('widgetNoDrag');
|
||||
expect(screen.queryByRole('heading')).not.toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /favorites\.listView/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /favorites\.gridView/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('entprellt das Speichern des Titels um 1500 ms', async () => {
|
||||
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
|
||||
try {
|
||||
render(
|
||||
<FavoritesWidget instanceId="fav-1" config={{}} isEditMode={true} />,
|
||||
);
|
||||
await act(async () => {});
|
||||
|
||||
const titleField = screen.getByPlaceholderText(
|
||||
'favorites.titlePlaceholder',
|
||||
);
|
||||
|
||||
for (const value of ['W', 'We', 'Werkzeuge']) {
|
||||
await act(async () => {
|
||||
fireEvent.change(titleField, { target: { value } });
|
||||
vi.advanceTimersByTime(200);
|
||||
});
|
||||
}
|
||||
expect(mockUpdateWidgetConfig).not.toHaveBeenCalled();
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(1500);
|
||||
});
|
||||
expect(mockUpdateWidgetConfig).toHaveBeenCalledTimes(1);
|
||||
expect(mockUpdateWidgetConfig).toHaveBeenCalledWith('fav-1', {
|
||||
title: 'Werkzeuge',
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(titleField, { target: { value: '' } });
|
||||
vi.advanceTimersByTime(1500);
|
||||
});
|
||||
expect(mockUpdateWidgetConfig).toHaveBeenCalledWith('fav-1', {
|
||||
title: '',
|
||||
});
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user