release: v0.20.0
This commit is contained in:
61
app/admin/__tests__/SortableTiles.test.tsx
Normal file
61
app/admin/__tests__/SortableTiles.test.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { render, screen, cleanup } from '@testing-library/react';
|
||||
import { DndContext } from '@dnd-kit/core';
|
||||
import { SortableContext } from '@dnd-kit/sortable';
|
||||
import { SortablePageRow, SortableHeroTile } from '../components/page-builder/SortableTiles';
|
||||
import { dndAnnouncements } from '../components/dndAnnouncements';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
/**
|
||||
* QA A-20: the sortable attributes (role="button", tabindex) sat on the whole
|
||||
* row, which also holds the row's own button — nested interactive content. They
|
||||
* belong on the drag handle. And the drag announcements read out internal ids.
|
||||
*/
|
||||
describe('sortable rows', () => {
|
||||
it('put the sortable role on the handle, not around the row button', () => {
|
||||
render(
|
||||
<DndContext>
|
||||
<SortableContext items={['page-pricing']}>
|
||||
<SortablePageRow id="page-pricing" title="Pricing" isActive={false} onClick={() => {}} />
|
||||
</SortableContext>
|
||||
</DndContext>,
|
||||
);
|
||||
const handle = screen.getByLabelText('Reorder Pricing');
|
||||
expect(handle.getAttribute('role')).toBe('button');
|
||||
for (const button of screen.getAllByRole('button')) {
|
||||
expect(button.parentElement?.closest('[role="button"]')).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps the hero tile remove button out of the handle', () => {
|
||||
render(
|
||||
<DndContext>
|
||||
<SortableContext items={['hero-0']}>
|
||||
<SortableHeroTile id="abc" index={0} onRemove={() => {}} />
|
||||
</SortableContext>
|
||||
</DndContext>,
|
||||
);
|
||||
const remove = screen.getByTitle('Remove');
|
||||
expect(remove.parentElement?.closest('[role="button"]')).toBeNull();
|
||||
expect(screen.getByLabelText('Reorder hero photo 1').getAttribute('role')).toBe('button');
|
||||
});
|
||||
});
|
||||
|
||||
describe('dndAnnouncements', () => {
|
||||
const label = (id: string | number) => (id === 'page-pricing' ? 'page "Pricing"' : 'page "FAQ"');
|
||||
const a = dndAnnouncements(label);
|
||||
const active = { id: 'page-pricing' } as never;
|
||||
const over = { id: 'subpage-3' } as never;
|
||||
|
||||
it('speaks the names the list shows, not the ids', () => {
|
||||
expect(a.onDragStart({ active })).toBe('Picked up page "Pricing".');
|
||||
expect(a.onDragEnd({ active, over } as never)).toBe(
|
||||
'Page "Pricing" was dropped at the position of page "FAQ".',
|
||||
);
|
||||
expect(a.onDragCancel({ active, over: null } as never)).toBe(
|
||||
'Moving page "Pricing" was cancelled.',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -35,6 +35,7 @@ const everyOption: Required<Omit<AlbumEntryObject, 'grid'>> & Pick<AlbumEntryObj
|
||||
assetOrder: ['33333333-3333-3333-3333-333333333333'],
|
||||
coverPosition: '50% 25%',
|
||||
download: true,
|
||||
zoom: false,
|
||||
location: 'hidden',
|
||||
grid: { columns: 4, gap: 8, aspectRatio: '3/2', layout: 'masonry' },
|
||||
};
|
||||
@@ -47,6 +48,17 @@ describe('album entry round trip', () => {
|
||||
expect(serialized).toEqual({ [uuid]: everyOption });
|
||||
});
|
||||
|
||||
it('keeps zoom: false, which switches zoom off for the album (#467)', () => {
|
||||
const parsed = parseAlbumEntries([{ [uuid]: { zoom: false } }]);
|
||||
expect(parsed[0].zoom).toBe(false);
|
||||
expect(hasAlbumOptions(parsed[0])).toBe(true);
|
||||
expect(serializeAlbumEntries(parsed)).toEqual([{ [uuid]: { zoom: false } }]);
|
||||
});
|
||||
|
||||
it('writes no zoom key while the album inherits', () => {
|
||||
expect(serializeAlbumEntries([{ id: uuid, zoom: undefined }])).toEqual([uuid]);
|
||||
});
|
||||
|
||||
it('keeps map precision across a save', () => {
|
||||
const parsed = parseAlbumEntries([{ [uuid]: { location: 'city' } }]);
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('hero, subpages and sections', () => {
|
||||
|
||||
it('names a new subpage after its position and starts it empty', () => {
|
||||
const g = ops.addSubpage(state());
|
||||
expect(g.subpages[3]).toEqual({ name: 'New Page 4', albums: [], sections: undefined });
|
||||
expect(g.subpages[3]).toEqual({ name: 'New subpage 4', albums: [], sections: undefined });
|
||||
});
|
||||
|
||||
it('adds, updates and removes sections', () => {
|
||||
|
||||
@@ -138,6 +138,7 @@ const everySubpageKey: Required<SubpageYaml> = {
|
||||
sections: [{ title: 'North', description: 'Cold', albums: [B] }],
|
||||
password: 'scrypt:aa:bb',
|
||||
proofing: false,
|
||||
zoom: true,
|
||||
essayFile: 'story',
|
||||
essayText: 'Text',
|
||||
enabled: false,
|
||||
@@ -161,6 +162,18 @@ describe('subpage keys', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('subpage zoom (#467)', () => {
|
||||
it('keeps zoom in either direction, and writes nothing when unset', () => {
|
||||
const off = { name: 'Z', zoom: false, albums: [A] };
|
||||
const on = { name: 'Z', zoom: true, albums: [A] };
|
||||
expect(roundTrip({ subpages: [off] })).toEqual({ subpages: [off] });
|
||||
expect(roundTrip({ subpages: [on] })).toEqual({ subpages: [on] });
|
||||
expect(roundTrip({ subpages: [{ name: 'Z', albums: [A] }] })).toEqual({
|
||||
subpages: [{ name: 'Z', albums: [A] }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('content page references (#722)', () => {
|
||||
it('keeps `- page:` entries where they were among the subpages', () => {
|
||||
const yaml = {
|
||||
|
||||
35
app/admin/__tests__/sameDraft.test.ts
Normal file
35
app/admin/__tests__/sameDraft.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { sameDraft } from '../components/sameDraft';
|
||||
|
||||
/**
|
||||
* "Unsaved changes" stayed up after an edit was typed back to the saved value
|
||||
* (QA A-19): the editors only ever set their dirty flag. They now also compare
|
||||
* with the saved state, which has to see through what an undone edit leaves.
|
||||
*/
|
||||
describe('sameDraft', () => {
|
||||
it('is false for a real change', () => {
|
||||
expect(sameDraft({ title: 'A' }, { title: 'B' })).toBe(false);
|
||||
expect(sameDraft({ navLinks: [{ label: 'A' }] }, { navLinks: [] })).toBe(false);
|
||||
expect(sameDraft({ albums: ['a', 'b'] }, { albums: ['b', 'a'] })).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores key order — a deleted key typed again moves to the end', () => {
|
||||
expect(
|
||||
sameDraft(
|
||||
{ theme: { accent: '#fff', preset: 'noir' } },
|
||||
{
|
||||
theme: { preset: 'noir', accent: '#fff' },
|
||||
},
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('ignores the empty parent objects a settings edit creates', () => {
|
||||
expect(sameDraft({ title: 'A', contact: {} }, { title: 'A' })).toBe(true);
|
||||
expect(sameDraft({ theme: { fonts: {} } }, {})).toBe(true);
|
||||
});
|
||||
|
||||
it('ignores undefined, which the YAML never holds', () => {
|
||||
expect(sameDraft({ title: 'A', subtitle: undefined }, { title: 'A' })).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user