mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
The archive page's six actions shared edges, and Update sat lower than the rest. Both come from the same thing: Semantic's Button carried `margin: 0 .25em .25em 0`, so a page could list buttons as bare siblings and they arrived spaced -- and separated from the line below when the row wrapped. Nothing replaced that margin. Two of those six answered with `marginTop: 0.5em`, which is the wrong axis: a Button is `display: inline-flex; vertical-align: middle`, and `vertical-align` is measured on the MARGIN box, so half of any top margin shows up as the visible button dropping. It was covering the wrapped-row case in the only axis a margin can, and paying for it in the other one. `.wrolpi-button-row` is a flex row with a gap, so both axes are covered at once and no button needs a margin of its own -- the same rule as `.wrolpi-stack`, along the other axis. Applied to the five rows in the app that had no gap: the archive page, the shared collection edit form (which three pages hand action buttons to, and where nine buttons plus Cancel carried `margin-top: 1em` each while Save carried none), the doc page's two responsive branches, and the downloads table's edit/restart pair. `.action-button-spacing` goes with them; it was that same margin, on Cancel. Guarded four ways. ui-layout.cy.js measures that the row really spaces what it holds, wraps rather than overflows, centres its buttons against a taller item beside them, and counts a confirmed APIButton as one item rather than two -- that one returns a fragment holding a `<Confirm/>`, which would open a hole in the row if it did not portal. Common.test.js scans for the two ways this comes back: a button in a row stamping a vertical margin, and a row of buttons that is not a row. Neither throws, which is how the app got here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
280 lines
9.5 KiB
JavaScript
280 lines
9.5 KiB
JavaScript
import React from 'react';
|
|
import {
|
|
createMockDomain,
|
|
createTestForm,
|
|
render,
|
|
renderInDarkMode,
|
|
renderInLightMode,
|
|
screen
|
|
} from '../../test-utils';
|
|
import {CollectionEditForm} from './CollectionEditForm';
|
|
|
|
// Mock the TagsContext
|
|
jest.mock('../../Tags', () => ({
|
|
TagsContext: {
|
|
_currentValue: {
|
|
SingleTag: ({name}) => <span data-testid="applied-tag">{name}</span>
|
|
}
|
|
},
|
|
}));
|
|
|
|
// Mock Common components
|
|
jest.mock('../Common', () => ({
|
|
...jest.requireActual('../Common'),
|
|
WROLModeMessage: ({content}) => <div data-testid="wrol-mode-message">{content}</div>,
|
|
}));
|
|
|
|
describe('CollectionEditForm', () => {
|
|
const mockCollection = createMockDomain();
|
|
|
|
describe('Form Rendering', () => {
|
|
it('renders children and Save button', () => {
|
|
const form = createTestForm(mockCollection);
|
|
|
|
render(
|
|
<CollectionEditForm form={form}>
|
|
<div data-testid="child-content">Form fields here</div>
|
|
</CollectionEditForm>
|
|
);
|
|
|
|
expect(screen.getByTestId('child-content')).toBeInTheDocument();
|
|
expect(screen.getByRole('button', {name: /save/i})).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders title when provided', () => {
|
|
const form = createTestForm(mockCollection);
|
|
|
|
render(
|
|
<CollectionEditForm form={form} title="Edit Domain: example.com">
|
|
<div>Form content</div>
|
|
</CollectionEditForm>
|
|
);
|
|
|
|
expect(screen.getByRole('heading', {level: 1})).toHaveTextContent('Edit Domain: example.com');
|
|
});
|
|
|
|
it('renders WROL mode message when provided', () => {
|
|
const form = createTestForm(mockCollection);
|
|
|
|
render(
|
|
<CollectionEditForm
|
|
form={form}
|
|
wrolModeContent="Editing disabled in WROL Mode"
|
|
>
|
|
<div>Form content</div>
|
|
</CollectionEditForm>
|
|
);
|
|
|
|
expect(screen.getByTestId('wrol-mode-message')).toHaveTextContent('Editing disabled in WROL Mode');
|
|
});
|
|
|
|
it('renders action buttons when provided', () => {
|
|
const form = createTestForm(mockCollection);
|
|
|
|
render(
|
|
<CollectionEditForm
|
|
form={form}
|
|
actionButtons={<button data-testid="delete-button">Delete</button>}
|
|
>
|
|
<div>Form content</div>
|
|
</CollectionEditForm>
|
|
);
|
|
|
|
expect(screen.getByTestId('delete-button')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders appliedTagName when provided', () => {
|
|
const form = createTestForm(mockCollection);
|
|
|
|
render(
|
|
<CollectionEditForm form={form} appliedTagName="News">
|
|
<div>Form content</div>
|
|
</CollectionEditForm>
|
|
);
|
|
|
|
expect(screen.getByTestId('applied-tag')).toHaveTextContent('News');
|
|
});
|
|
|
|
it('does not render appliedTagName when not provided', () => {
|
|
const form = createTestForm(mockCollection);
|
|
|
|
render(
|
|
<CollectionEditForm form={form}>
|
|
<div>Form content</div>
|
|
</CollectionEditForm>
|
|
);
|
|
|
|
expect(screen.queryByTestId('applied-tag')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Save/Cancel Actions', () => {
|
|
it('shows Cancel button when onCancel provided', () => {
|
|
const form = createTestForm(mockCollection);
|
|
const mockOnCancel = jest.fn();
|
|
|
|
render(
|
|
<CollectionEditForm form={form} onCancel={mockOnCancel}>
|
|
<div>Form content</div>
|
|
</CollectionEditForm>
|
|
);
|
|
|
|
expect(screen.getByRole('button', {name: /cancel/i})).toBeInTheDocument();
|
|
});
|
|
|
|
it('does not show Cancel button when onCancel not provided', () => {
|
|
const form = createTestForm(mockCollection);
|
|
|
|
render(
|
|
<CollectionEditForm form={form}>
|
|
<div>Form content</div>
|
|
</CollectionEditForm>
|
|
);
|
|
|
|
expect(screen.queryByRole('button', {name: /cancel/i})).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('disables Save button when form is disabled', () => {
|
|
const form = createTestForm(mockCollection, {
|
|
overrides: {disabled: true}
|
|
});
|
|
|
|
render(
|
|
<CollectionEditForm form={form}>
|
|
<div>Form content</div>
|
|
</CollectionEditForm>
|
|
);
|
|
|
|
expect(screen.getByRole('button', {name: /save/i})).toBeDisabled();
|
|
});
|
|
|
|
it('disables Cancel button when form is disabled', () => {
|
|
const form = createTestForm(mockCollection, {
|
|
overrides: {disabled: true}
|
|
});
|
|
const mockOnCancel = jest.fn();
|
|
|
|
render(
|
|
<CollectionEditForm form={form} onCancel={mockOnCancel}>
|
|
<div>Form content</div>
|
|
</CollectionEditForm>
|
|
);
|
|
|
|
expect(screen.getByRole('button', {name: /cancel/i})).toBeDisabled();
|
|
});
|
|
});
|
|
|
|
describe('Error States', () => {
|
|
it('displays form-level errors', () => {
|
|
const form = createTestForm(mockCollection, {
|
|
overrides: {error: 'Something went wrong'}
|
|
});
|
|
|
|
render(
|
|
<CollectionEditForm form={form}>
|
|
<div>Form content</div>
|
|
</CollectionEditForm>
|
|
);
|
|
|
|
expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it('does not display error message when no error', () => {
|
|
const form = createTestForm(mockCollection);
|
|
|
|
render(
|
|
<CollectionEditForm form={form}>
|
|
<div>Form content</div>
|
|
</CollectionEditForm>
|
|
);
|
|
|
|
expect(screen.queryByText(/error/i)).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Form Submission', () => {
|
|
it('calls form.onSubmit when Save button is clicked', () => {
|
|
const form = createTestForm(mockCollection);
|
|
form.onSubmit = jest.fn();
|
|
|
|
render(
|
|
<CollectionEditForm form={form}>
|
|
<div>Form content</div>
|
|
</CollectionEditForm>
|
|
);
|
|
|
|
screen.getByRole('button', {name: /save/i}).click();
|
|
expect(form.onSubmit).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Theme Integration', () => {
|
|
// Panel/Header are token-driven (see ui/Surfaces.tsx): their colors come from CSS
|
|
// variables scoped on `[data-theme]`, not from JS branching on the theme. There is
|
|
// no more "inverted" prop/class to assert on -- just that the same markup renders
|
|
// in both modes.
|
|
it('renders a token-driven Panel in dark mode', () => {
|
|
const form = createTestForm(mockCollection);
|
|
|
|
const {container} = renderInDarkMode(
|
|
<CollectionEditForm form={form}>
|
|
<div>Form content</div>
|
|
</CollectionEditForm>
|
|
);
|
|
|
|
expect(container.querySelector('.wrolpi-panel')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders a token-driven Panel in light mode', () => {
|
|
const form = createTestForm(mockCollection);
|
|
|
|
const {container} = renderInLightMode(
|
|
<CollectionEditForm form={form}>
|
|
<div>Form content</div>
|
|
</CollectionEditForm>
|
|
);
|
|
|
|
expect(container.querySelector('.wrolpi-panel')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders a token-driven Header for the title', () => {
|
|
const form = createTestForm(mockCollection);
|
|
|
|
const {container} = renderInDarkMode(
|
|
<CollectionEditForm form={form} title="Test Collection">
|
|
<div>Form content</div>
|
|
</CollectionEditForm>
|
|
);
|
|
|
|
const header = container.querySelector('.wrolpi-header');
|
|
expect(header).toBeInTheDocument();
|
|
expect(header).toHaveTextContent('Test Collection');
|
|
});
|
|
});
|
|
|
|
describe('CSS Classes', () => {
|
|
it('puts its actions in a button row, and leaves their margins alone', () => {
|
|
/*
|
|
* `action-button-spacing` was `margin-top: 1em` on Cancel alone, matching the same
|
|
* margin the three calling pages stamped on each of their action buttons -- so every
|
|
* button in this row sat 1em below the Save beside it, which has none. The row spaces
|
|
* and wraps them now, and nothing in it carries a margin of its own.
|
|
*/
|
|
const form = createTestForm(mockCollection);
|
|
const mockOnCancel = jest.fn();
|
|
|
|
const {container} = render(
|
|
<CollectionEditForm form={form} onCancel={mockOnCancel}>
|
|
<div>Form content</div>
|
|
</CollectionEditForm>
|
|
);
|
|
|
|
const cancelButton = screen.getByRole('button', {name: /cancel/i});
|
|
const row = container.querySelector('.wrolpi-button-row');
|
|
expect(row).toBeInTheDocument();
|
|
expect(row).toContainElement(cancelButton);
|
|
expect(row).toContainElement(screen.getByRole('button', {name: /save/i}));
|
|
expect(cancelButton).not.toHaveClass('action-button-spacing');
|
|
});
|
|
});
|
|
});
|