// @vitest-environment jsdom import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, cleanup, fireEvent } from '@testing-library/react'; import SaveBar from '../components/SaveBar'; /** * The first component test in the project, and deliberately a small one: it is * here to show the shape (`// @vitest-environment jsdom` on the first line, * render, assert, cleanup) as much as to cover SaveBar. * * SaveBar earns it though. It decides whether an outcome reads as success or * failure by testing whether the message happens to start with "Error" — #600 * replaces that with a typed state, and this pins the behaviour so the swap can * be verified rather than eyeballed. */ afterEach(cleanup); const props = { dirty: false, saving: false, saveMessage: '', onSave: () => {}, label: 'Save', }; describe('SaveBar', () => { it('stays out of the way when there is nothing to act on', () => { const { container } = render(); // It is pinned over the page, so rendering it with nothing to say would // cover content for no reason. expect(container.firstChild).toBeNull(); }); it('announces unsaved changes', () => { render(); expect(screen.getByText('Unsaved changes')).toBeTruthy(); }); it('only offers saving when there is something to save', () => { const { rerender } = render(); expect(screen.getByRole('button', { name: 'Save' })).toHaveProperty('disabled', true); rerender(); expect(screen.getByRole('button', { name: 'Save' })).toHaveProperty('disabled', false); }); it('blocks a second save while one is in flight', () => { render(); const button = screen.getByRole('button', { name: 'Saving...' }); expect(button).toHaveProperty('disabled', true); }); it('calls onSave when clicked', () => { const onSave = vi.fn(); render(); fireEvent.click(screen.getByRole('button', { name: 'Save' })); expect(onSave).toHaveBeenCalledOnce(); }); it('tells a failure from a success by the message prefix', () => { // Pinned as it is, not as it should be: the prefix check is the thing #600 // replaces. A message that fails without starting with "Error" is styled as // a success today, which is precisely the argument for a typed state. const { rerender } = render(); expect(screen.getByText('Error: Failed to save').className).toContain('error'); rerender(); expect(screen.getByText('Saved!').className).toContain('success'); rerender(); expect(screen.getByText('Could not reach the server').className).toContain('success'); }); it('offers the preview link only where it belongs', () => { const { rerender } = render(); expect(screen.queryByRole('link')).toBeNull(); rerender(); // `?fresh=1` is what bypasses the cache, so the preview shows the save. expect(screen.getByRole('link')).toHaveProperty('href', expect.stringContaining('/?fresh=1')); }); });