Skip to content

Commit 9c82d92

Browse files
hotlongclaude
andcommitted
test(components): pin the page:header trailing host slot
Claude-Session: https://claude.ai/code/session_cdb7a717def74e719988f089fbaa18ff Co-authored-by: Claude <noreply@anthropic.com>
1 parent 421fa4e commit 9c82d92

1 file changed

Lines changed: 89 additions & 0 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectstack#27241 — the host slot at the end of `page:header`'s action row.
11+
* The console's record view hands in the reader's panel switches through
12+
* `PageHeaderTrailingContext`; the row draws them as its LAST segment, after
13+
* the `⋯` menu, behind a vertical hairline, and wraps inside the header
14+
* (`max-w-full`) rather than running past a narrow page. No slot, no change:
15+
* the row is what it was.
16+
*/
17+
18+
import { describe, it, expect, afterEach } from 'vitest';
19+
import { render, cleanup } from '@testing-library/react';
20+
import { ComponentRegistry } from '@object-ui/core';
21+
import { ActionProvider, RecordContextProvider } from '@object-ui/react';
22+
import { PageHeaderActions, PageHeaderTrailingContext } from '../index';
23+
24+
const RECORD = { id: 'lead-1', name: 'Ada' };
25+
const SYSTEM = [
26+
{ name: 'sys_edit', label: 'Edit', type: 'script', locations: ['record_header'], order: 100, onClick: () => {} },
27+
{ name: 'sys_delete', label: 'Delete', type: 'script', locations: ['record_header'], order: 120, component: 'action:menu', onClick: () => {} },
28+
];
29+
30+
function inRecord(node: React.ReactNode, headerSystemActions: unknown[] = SYSTEM) {
31+
return (
32+
<ActionProvider>
33+
<RecordContextProvider
34+
objectName="lead"
35+
recordId={RECORD.id}
36+
data={RECORD}
37+
objectSchema={{ name: 'lead', label: 'Lead' }}
38+
headerSystemActions={headerSystemActions as never}
39+
>
40+
{node}
41+
</RecordContextProvider>
42+
</ActionProvider>
43+
);
44+
}
45+
46+
const SWITCHES = <div data-testid="switches">switches</div>;
47+
48+
afterEach(() => cleanup());
49+
50+
describe('page:header — the trailing host slot (objectstack#27241)', () => {
51+
it('draws the slot as the toolbar’s last segment, after ⋯, behind a vertical hairline', () => {
52+
const PageHeader = ComponentRegistry.get('page:header')!;
53+
const { container, getByTestId } = render(
54+
inRecord(
55+
<PageHeaderTrailingContext.Provider value={SWITCHES}>
56+
<PageHeader schema={{ type: 'page:header' }} />
57+
</PageHeaderTrailingContext.Provider>,
58+
),
59+
);
60+
const toolbar = container.querySelector('[role="toolbar"]')!;
61+
const trailing = toolbar.lastElementChild!;
62+
expect(trailing.hasAttribute('data-page-header-trailing')).toBe(true);
63+
expect(trailing.contains(getByTestId('switches'))).toBe(true);
64+
const rule = trailing.querySelector('[role="separator"]')!;
65+
expect(rule.getAttribute('aria-orientation')).toBe('vertical');
66+
// The ⋯ trigger comes before the segment.
67+
const more = toolbar.querySelector('[aria-label="More actions"]')!;
68+
expect(more.compareDocumentPosition(trailing) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
69+
// The row wraps inside the header instead of overflowing it.
70+
expect(toolbar.className.split(/\s+/)).toEqual(expect.arrayContaining(['flex-wrap', 'shrink-0', 'max-w-full']));
71+
});
72+
73+
it('with no action the row is the slot alone, with no hairline', () => {
74+
const { container, getByTestId } = render(inRecord(<PageHeaderActions trailing={SWITCHES} />, []));
75+
const toolbar = container.querySelector('[role="toolbar"]')!;
76+
expect(toolbar.contains(getByTestId('switches'))).toBe(true);
77+
expect(toolbar.querySelector('[role="separator"]')).toBeNull();
78+
});
79+
80+
it('no slot: no trailing segment, and the empty slot still stands in for an empty row', () => {
81+
const PageHeader = ComponentRegistry.get('page:header')!;
82+
const withActions = render(inRecord(<PageHeader schema={{ type: 'page:header' }} />));
83+
expect(withActions.container.querySelector('[data-page-header-trailing]')).toBeNull();
84+
cleanup();
85+
const empty = render(inRecord(<PageHeaderActions emptySlot={<span data-testid="empty" />} />, []));
86+
expect(empty.container.querySelector('[role="toolbar"]')).toBeNull();
87+
expect(empty.getByTestId('empty')).toBeTruthy();
88+
});
89+
});

0 commit comments

Comments
 (0)