一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
198 行
8.1 KiB
TypeScript
198 行
8.1 KiB
TypeScript
import { LayoutItem, LayoutItemDirection } from './types';
|
|
import {
|
|
buildResizeSnapshot,
|
|
computeEdges,
|
|
isSubtreeFlexible,
|
|
lastVisibleChildIndex,
|
|
nextVisibleSibling,
|
|
planResize,
|
|
ResizeStartSnapshot,
|
|
} from './resizeLogic';
|
|
|
|
describe('resizeLogic', () => {
|
|
|
|
const sampleSizes = {
|
|
sideBar: { width: 250, height: 800 },
|
|
noteList: { width: 400, height: 800 },
|
|
editor: { width: 300, height: 800 },
|
|
chatPanel: { width: 340, height: 800 },
|
|
};
|
|
|
|
test.each([
|
|
['flag directly on item', { key: 'a', flexible: true } as LayoutItem, true],
|
|
['flag on nested descendant', { key: 'a', children: [{ key: 'b', children: [{ key: 'c', flexible: true }] }] } as LayoutItem, true],
|
|
['no flag anywhere in subtree', { key: 'a', children: [{ key: 'b' }] } as LayoutItem, false],
|
|
['leaf without flag', { key: 'a' } as LayoutItem, false],
|
|
])('isSubtreeFlexible: %s', (_label, item, expected) => {
|
|
expect(isSubtreeFlexible(item)).toBe(expected);
|
|
});
|
|
|
|
test('nextVisibleSibling skips hidden siblings in normal mode', () => {
|
|
const parent: LayoutItem = {
|
|
key: 'root',
|
|
children: [
|
|
{ key: 'a' },
|
|
{ key: 'b', visible: false },
|
|
{ key: 'c' },
|
|
],
|
|
};
|
|
expect(nextVisibleSibling(parent.children[0], parent, false)?.key).toBe('c');
|
|
});
|
|
|
|
test('nextVisibleSibling includes hidden siblings in moveMode', () => {
|
|
const parent: LayoutItem = {
|
|
key: 'root',
|
|
children: [
|
|
{ key: 'a' },
|
|
{ key: 'b', visible: false },
|
|
{ key: 'c' },
|
|
],
|
|
};
|
|
expect(nextVisibleSibling(parent.children[0], parent, true)?.key).toBe('b');
|
|
});
|
|
|
|
test('lastVisibleChildIndex is moveMode-aware', () => {
|
|
const item: LayoutItem = {
|
|
key: 'root',
|
|
children: [{ key: 'a' }, { key: 'b' }, { key: 'c', visible: false }],
|
|
};
|
|
expect(lastVisibleChildIndex(item, false)).toBe(1);
|
|
expect(lastVisibleChildIndex(item, true)).toBe(2);
|
|
});
|
|
|
|
test('computeEdges: middle row child gets an own right edge only', () => {
|
|
const parent: LayoutItem = { key: 'root', direction: LayoutItemDirection.Row };
|
|
expect(computeEdges(parent, false)).toEqual({ ownRight: true, ownBottom: false, parentRight: false, parentBottom: false });
|
|
});
|
|
|
|
test('computeEdges: last row child inherits parent right', () => {
|
|
const parent: LayoutItem = { key: 'root', direction: LayoutItemDirection.Row };
|
|
const parentEdges = { ownRight: false, ownBottom: false, parentRight: true, parentBottom: false };
|
|
expect(computeEdges(parent, true, parentEdges)).toEqual({ ownRight: false, ownBottom: false, parentRight: true, parentBottom: false });
|
|
});
|
|
|
|
test('computeEdges: every column child sits at the column right edge, so parentRight propagates through', () => {
|
|
const parent: LayoutItem = { key: 'col', direction: LayoutItemDirection.Column };
|
|
const parentEdges = { ownRight: true, ownBottom: false, parentRight: false, parentBottom: false };
|
|
expect(computeEdges(parent, false, parentEdges).parentRight).toBe(true);
|
|
});
|
|
|
|
test('computeEdges: same-axis nesting propagates outer own-right to a last row-in-row child', () => {
|
|
// Unusual but valid nesting: the inner container's right edge
|
|
// coincides with the outer's, so the outer's own right divider gap
|
|
// must flow to the inner's descendants.
|
|
const parent: LayoutItem = { key: 'inner', direction: LayoutItemDirection.Row };
|
|
const parentEdges = { ownRight: true, ownBottom: false, parentRight: false, parentBottom: false };
|
|
expect(computeEdges(parent, true, parentEdges).parentRight).toBe(true);
|
|
});
|
|
|
|
test('buildResizeSnapshot: only truly-flexible subtrees are flagged as absorbers', () => {
|
|
const parent: LayoutItem = { key: 'root', direction: LayoutItemDirection.Row, children: [
|
|
{ key: 'sideBar', width: 250 },
|
|
{ key: 'editor', flexible: true },
|
|
] };
|
|
const snap = buildResizeSnapshot(parent.children[0], parent, false, sampleSizes);
|
|
expect(snap.itemAbsorbsAlongAxis.width).toBe(false); // sideBar
|
|
expect(snap.nextAbsorbsAlongAxis.width).toBe(true); // editor
|
|
});
|
|
|
|
test('buildResizeSnapshot: width-less non-flexible next sibling is not an absorber', () => {
|
|
// Regression: dragging noteList↔chatPanel where chatPanel is unsized
|
|
// in state but not marked flexible. Old code treated any unsized
|
|
// panel as an absorber and skipped writes to it.
|
|
const parent: LayoutItem = { key: 'root', direction: LayoutItemDirection.Row, children: [
|
|
{ key: 'noteList', width: 400 },
|
|
{ key: 'chatPanel' },
|
|
] };
|
|
const snap = buildResizeSnapshot(parent.children[0], parent, false, sampleSizes);
|
|
expect(snap.nextAbsorbsAlongAxis.width).toBe(false);
|
|
});
|
|
|
|
test('buildResizeSnapshot: picks the next sibling that is hidden in state when in moveMode', () => {
|
|
const parent: LayoutItem = { key: 'root', direction: LayoutItemDirection.Row, children: [
|
|
{ key: 'noteList', width: 400 },
|
|
{ key: 'chatPanel', visible: false },
|
|
] };
|
|
const snap = buildResizeSnapshot(parent.children[0], parent, true, sampleSizes);
|
|
expect(snap.nextSiblingKey).toBe('chatPanel');
|
|
});
|
|
|
|
const baseSnap = (overrides: Partial<ResizeStartSnapshot> = {}): ResizeStartSnapshot => ({
|
|
key: 'a',
|
|
initialWidth: 300,
|
|
initialHeight: 200,
|
|
nextSiblingKey: 'b',
|
|
nextSiblingInitialWidth: 400,
|
|
nextSiblingInitialHeight: 200,
|
|
itemAbsorbsAlongAxis: { width: false, height: false },
|
|
nextAbsorbsAlongAxis: { width: false, height: false },
|
|
...overrides,
|
|
});
|
|
|
|
test('planResize: drag right by 50 grows the dragged panel and shrinks the next', () => {
|
|
expect(planResize(baseSnap(), 'right', { width: 50, height: 0 })).toEqual([
|
|
{ key: 'a', props: { width: 350 } },
|
|
{ key: 'b', props: { width: 350 } },
|
|
]);
|
|
});
|
|
|
|
test('planResize: skips write to the next sibling when it is the absorber', () => {
|
|
const snap = baseSnap({ nextAbsorbsAlongAxis: { width: true, height: false } });
|
|
expect(planResize(snap, 'right', { width: 50, height: 0 })).toEqual([
|
|
{ key: 'a', props: { width: 350 } },
|
|
]);
|
|
});
|
|
|
|
test('planResize: redirects to the next sibling (inverse) when dragged is the absorber', () => {
|
|
// Editor is flexible and unsized; dragging its right edge should
|
|
// resize the panel to its right instead so the editor stays flex.
|
|
const snap = baseSnap({ itemAbsorbsAlongAxis: { width: true, height: false } });
|
|
expect(planResize(snap, 'right', { width: 50, height: 0 })).toEqual([
|
|
{ key: 'b', props: { width: 350 } },
|
|
]);
|
|
});
|
|
|
|
test('planResize: writes to the dragged panel when both sides are absorbers (#15934)', () => {
|
|
// Sidebar and note list stacked in a column, both unsized. Without
|
|
// the bothAbsorb branch, neither was written and the divider never
|
|
// moved.
|
|
const snap = baseSnap({
|
|
itemAbsorbsAlongAxis: { width: false, height: true },
|
|
nextAbsorbsAlongAxis: { width: false, height: true },
|
|
});
|
|
expect(planResize(snap, 'bottom', { width: 0, height: 40 })).toEqual([
|
|
{ key: 'a', props: { height: 240 } },
|
|
]);
|
|
});
|
|
|
|
test('planResize: no next sibling means only the dragged is written', () => {
|
|
const snap = baseSnap({ nextSiblingKey: null });
|
|
expect(planResize(snap, 'right', { width: 50, height: 0 })).toEqual([
|
|
{ key: 'a', props: { width: 350 } },
|
|
]);
|
|
});
|
|
|
|
test('planResize: clamps a shared delta so both sides respect min and the combined size is preserved', () => {
|
|
const snap = baseSnap();
|
|
const originalSum = snap.initialWidth + snap.nextSiblingInitialWidth;
|
|
const updates = planResize(snap, 'right', { width: -500, height: 0 });
|
|
// itemMinWidth is 40; dragged clamps to 40, next grows by the same
|
|
// amount so the two sizes sum to the original combined width.
|
|
expect(updates[0].props.width).toBe(40);
|
|
expect(updates[1].props.width).toBe(originalSum - 40);
|
|
expect(updates[0].props.width + updates[1].props.width).toBe(originalSum);
|
|
});
|
|
|
|
test('planResize: preserves the combined size when the delta would push the next sibling below min', () => {
|
|
const snap = baseSnap();
|
|
const originalSum = snap.initialWidth + snap.nextSiblingInitialWidth;
|
|
const updates = planResize(snap, 'right', { width: 500, height: 0 });
|
|
// The next sibling clamps to itemMinWidth (40); the dragged panel
|
|
// grows by exactly the same amount so the sum is preserved.
|
|
expect(updates[1].props.width).toBe(40);
|
|
expect(updates[0].props.width).toBe(originalSum - 40);
|
|
expect(updates[0].props.width + updates[1].props.width).toBe(originalSum);
|
|
});
|
|
|
|
});
|