Skip to content

Commit d98d3e2

Browse files
fix(plugin-detail): record page read mode wraps text values; highlighted dates keep their day (#25998)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi999fd6vMjWTq3UYxHRARrsL)_ Fixes #25897 Clause-②: no Metadata-protocol: none ## What changed Ruling (triage 6407256029, verbatim): "Read mode wraps text-like values; truncation only where a layout declares it, with the full value reachable." - **Details body, read mode** (`ui/packages/plugin-detail/src/DetailSection.tsx`). A value the cell table sends to the grid's one-line `TextCellRenderer` (`TruncatedText`: `truncate`, one line, an ellipsis) now reads through `TextField`'s own read display, beside the existing `textarea` branch that reads through `TextAreaField`'s. Matched by renderer identity, like the boolean branch next to it: every type the table maps to the text cell (`text`, `code`, `qrcode`, `time`, `auto_number`) wraps, and a host that registered its own face for one of them keeps it. The value is coerced exactly as `TextCellRenderer` coerced it. `TruncatedText` itself is untouched, so grid cells keep truncating. - **Highlights strip** (`ui/packages/plugin-detail/src/HeaderHighlight.tsx`). `date` and `datetime` chips join the wide display types (email, url, phone, textarea) on the 16rem floor. This is the "let the cell grow" arm of the card's expectation. A chip whose line runs out still truncates, with the whole value as its `title`. - **`scripts/sdui-manifest.record.json`**: the registry source hash re-recorded by `node scripts/gen-sdui-manifest-node.mjs` (`check-sdui-manifest` hashes every registry source file). `sdui.manifest.json` is byte-identical. - Changeset: `.changeset/25897-record-read-mode-wraps-text.md` (`@object-ui/plugin-detail` patch). ## Premise check against main - Plain `text`: confirmed. `getCellRenderer('text')` is `TextCellRenderer`, which draws `TruncatedText` (`ui/packages/fields/src/index.tsx`). - `textarea`: already wraps on main (the objectui#11577 branch, released in 18.0.0 per the plugin-detail CHANGELOG, pinned by `record-details.textareaReadDisplay-11577`). The card's 18.1.0 reading of the article summary as one line does not reproduce from main for a field declared `textarea`; a summary declared `text` would read exactly as filed, and this PR covers that. - Highlights datetime: confirmed, measured. A 9rem chip leaves its value about 103px after `px-5` and the separator. Chromium (Inter, 14px semibold, the strip's value face) measured: | face | width | |---|---| | `2026/10/26 上午10:00` (compact datetime, zh-CN) | 154.8px | | `10/26/2026 10:00 am` (compact datetime, en-US) | 151.2px | | `Oct 26, 2026, 10:00 AM` (verbose datetime) | 163.9px | | `2026年10月26日` (date, zh-CN, year always written in the strip) | 114.5px | | `Oct 26, 2026` (date, en-US) | 89.5px | Every datetime face overruns the 9rem value box, and so does the zh-CN date face. The 16rem floor leaves 216px. `date` was included as a bounded same-class fix: same defect (a highlighted date losing its day), same file and same one-line rule. `DateTimeCellRenderer` is not changed. ## Tests - New `ui/packages/plugin-detail/src/renderers/__tests__/record-details.textReadWraps-25897.test.tsx`: on desktop and mobile, a `text` cell holds exactly `TextField`'s read markup and no one-line utility sits between the drawn subject and the row. An `auto_number` value reads the same way. Controls: the grid's `getCellRenderer('text')` is still `TextCellRenderer` with `truncate` and the full-value `title`, and a host-registered `text` face is kept. - New `ui/packages/plugin-detail/src/__tests__/HeaderHighlight.dateWideFloor-25897.test.tsx`: `datetime` and `date` chips sit on the 16rem floor. Controls: a plain `text` chip keeps 9rem, and the clip box keeps `truncate` with the whole value as its `title`. - Updated control in `record-details.textareaReadDisplay-11577.test.tsx`: it pinned "`text` keeps TextCellRenderer", which the ruling reverses. It now pins that a `text` row reads `TextField`'s display and never `TextAreaField`'s. - Reverse verification (fix committed at 94b2d39, both source files restored from its parent, then restored from HEAD; `git diff HEAD` empty afterwards): the three files went to 8 failed / 11 passed. The 8 failures are exactly the new behaviour assertions, and every control stayed green. - `pnpm exec vitest run packages/plugin-detail/` (from `ui/`): Test Files 303 passed, 1 skipped (304); Tests 2822 passed, 18 skipped. - `pnpm --filter @object-ui/plugin-detail run type-check`: exit 0. ## Gates (local, at f1aa0ba) - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 46 derived commands, all run, each exit 0, plus `node scripts/check-changeset-fixed.mjs` (its roster sits under `.changeset/`), exit 0. - `node scripts/pm/dispatch-gates.mjs --ran` with exit codes recorded: "46 derived famil(ies) accounted for, 46 run, 0 NOT-MEASURED (a DERIVED zero)". - `node scripts/check-sdui-manifest.mjs` was red at 9336bc0, because it hashes every registry source and this PR edits two. It is green after the re-record in f1aa0ba. - CI owns the type-check lanes, `Test Core`, the UI lint/type-check/build jobs and the repo-wide scans; they are not re-run here. ## Acceptance notes - Residual on the highlights strip: a row narrower than 16rem at `sm` and above (a very narrow host panel) still caps the chip at the row, so the value can truncate there. Below `sm` every chip is a full-width line (objectui#12523). - The link cells (email, url, phone) in a details section keep their own clip box with a full-value `title` (objectui#12523). That is truncation a renderer declares, with the full value reachable, so it is left as it is. - `markdown`, `html` and `richtext` read through the rich-text renderers and are not affected. --- _Generated by [Claude Code](https://claude.ai/code/session_01TQ7TyQSeddwHoL4mJ6wUyg)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d00215f commit d98d3e2

7 files changed

Lines changed: 357 additions & 9 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/plugin-detail': patch
3+
---
4+
5+
fix(plugin-detail): the record page's read mode wraps a text value instead of cutting it to one line, and a highlighted date keeps its day
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Details body.** A `text` field in a `record:details` section, read mode, was drawn with the grid's one-line cell (one line, an ellipsis), so a case subject read "Data export timing out for large…" in a two-column section with room to spare. It now reads through the text field's own read display and wraps, as a `textarea` already did. The same holds for every type that used the grid's text cell (`code`, `time`, `auto_number`, …). A host that registered its own cell renderer for one of those types keeps it. Grid cells are unchanged and still truncate, with the whole value on hover.
11+
- **Highlights strip.** A `date` or `datetime` chip now takes the wide column (16rem floor) that email, phone and URL chips take. On the 9rem column the compact datetime face, and a date in Chinese or Japanese, overran the value box, so the ellipsis landed inside the day ("2026/10/2…"). A chip whose line runs out still truncates, with the whole value as its hover title.
12+
13+
What to do: nothing.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"package":"ui/packages/plugin-detail","sources":54,"sha256":"21ccb887f5b2ccb7f48b871632b94f43e15f2264810fa6d08f6ba107367dd7f6","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}
1+
{"package":"ui/packages/plugin-detail","sources":54,"sha256":"c13b11a725295ab5e510c572d774271e3ec9a7120442907249bace960974f9bd","manifestSha256":"1f6e1ccb925a678eaa393f9091dfc7d0285e4e3626e0a5c61a5474687debabb7"}

‎ui/packages/plugin-detail/src/DetailSection.tsx‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import {
3838
RECORD_POINTER_CARD_TYPE,
3939
coerceToSafeValue,
4040
TextAreaField,
41+
TextCellRenderer,
42+
TextField,
4143
} from '@object-ui/fields';
4244
import type { DetailViewSection as DetailViewSectionType, DetailViewField, FieldMetadata } from '@object-ui/types';
4345
import { applyDetailAutoLayout } from './autoLayout';
@@ -102,8 +104,9 @@ export function getResponsiveSpanClass(span: number | undefined, columns: number
102104
}
103105

104106
/**
105-
* The `onChange` a read display is handed. `TextAreaField`'s `readonly` branch
106-
* renders no control and never calls it; the widget contract requires the slot.
107+
* The `onChange` a read display is handed. The `readonly` branches of
108+
* `TextAreaField` and `TextField` render no control and never call it; the
109+
* widget contract requires the slot.
107110
*/
108111
const ignoreReadOnlyChange = (): void => {};
109112

@@ -580,6 +583,28 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
580583
if (CellRenderer === BooleanCellRenderer && !fieldEditable && typeof value === 'boolean') {
581584
return booleanLabel(value);
582585
}
586+
// A value the table draws with the GRID's one-line text cell reads
587+
// through `TextField`'s own read display instead (objectstack#25897),
588+
// the same move the `textarea` branch above makes. `TextCellRenderer`
589+
// is `TruncatedText` — `truncate`, one line and an ellipsis — which
590+
// cut a case subject to "Data export timing out for large…" in a
591+
// two-column section with room to spare. Read mode wraps a text-like
592+
// value; truncation belongs to layouts that declare it, the grid row
593+
// and the highlights strip, and both keep it. Matched by renderer
594+
// identity, as the boolean above is, so every type the table maps to
595+
// the text cell (`text`, `code`, `time`, `auto_number`, …) wraps and a
596+
// host that registered its own face for one of them keeps that face.
597+
// The value is coerced exactly as `TextCellRenderer` coerced it.
598+
if (CellRenderer === TextCellRenderer) {
599+
return (
600+
<TextField
601+
field={enrichedField as unknown as FieldMetadata}
602+
value={String(coerceToSafeValue(value))}
603+
onChange={ignoreReadOnlyChange}
604+
readonly
605+
/>
606+
);
607+
}
583608
if (CellRenderer) {
584609
return (
585610
<CellRenderer

‎ui/packages/plugin-detail/src/HeaderHighlight.tsx‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,22 @@ export const HeaderHighlight: React.FC<HeaderHighlightProps> = ({
244244
// the HotCRM lead read "+1-555-0" that way. (2) is about the
245245
// editor, which the declared type picks, so it keeps
246246
// `resolvedType`.
247+
//
248+
// `date` and `datetime` joined (1) in objectstack#25897: the
249+
// strip writes every date's year (objectui#13331), and a 9rem chip
250+
// leaves its value about 103px after the padding. Chromium (Inter,
251+
// the strip's 14px semibold) read the compact `datetime` face at
252+
// 151-164px (`2026/10/26 上午10:00`, `10/26/2026 10:00 am`) and
253+
// the zh-CN `date` face at 114px (`2026年10月26日`), so the
254+
// ellipsis landed inside the day: "2026/10/2…". A date that loses
255+
// its day is worse than a wider chip; the 16rem floor leaves 216px.
247256
const isWide =
248257
drawnType === 'email' ||
249258
drawnType === 'url' ||
250259
drawnType === 'phone' ||
251260
drawnType === 'textarea' ||
261+
drawnType === 'date' ||
262+
drawnType === 'datetime' ||
252263
(!!resolvedType && EXPANDABLE_FIELD_TYPES.has(resolvedType));
253264
const isBoolean = resolvedType === 'boolean';
254265
// The SAME definition the body grid draws its em-dash from
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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#25897: the case record's highlight strip showed its SLA due
11+
* datetime as "2026/10/2…" — the ellipsis landed inside the day.
12+
*
13+
* A 9rem chip leaves its value about 103px once its padding and separator are
14+
* paid. Chromium (Inter, the strip's 14px semibold) read the compact
15+
* `datetime` face at 151-164px and the zh-CN `date` face at 114px, so both
16+
* date types joined the wide display types on the 16rem floor (216px). The
17+
* strip still declares truncation for a line that runs out, with the whole
18+
* value as the clip box's `title`; that half is unchanged and pinned here too.
19+
*
20+
* happy-dom has no layout engine, so these are class-semantics pins, the same
21+
* approach as the objectui#11684 and objectui#12523 pins beside this file.
22+
*/
23+
24+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
25+
import { render, screen, cleanup } from '@testing-library/react';
26+
import * as React from 'react';
27+
import type { HighlightField } from '@object-ui/types';
28+
import { HeaderHighlight } from '../HeaderHighlight';
29+
30+
const CLOCK = '2026-10-23T12:00:00.000Z';
31+
const SLA_DUE = '2026-10-26T02:00:00.000Z';
32+
33+
beforeEach(() => {
34+
vi.useFakeTimers({ toFake: ['Date'] });
35+
vi.setSystemTime(new Date(CLOCK));
36+
});
37+
38+
afterEach(() => {
39+
cleanup();
40+
vi.useRealTimers();
41+
});
42+
43+
/** The HotCRM case's strip, with its own field types. */
44+
const caseSchema = {
45+
fields: {
46+
priority: { type: 'text', label: 'Priority' },
47+
sla_due_date: { type: 'datetime', label: 'SLA Due' },
48+
opened_on: { type: 'date', label: 'Opened On' },
49+
},
50+
};
51+
52+
const fields: HighlightField[] = [
53+
{ name: 'priority', label: 'Priority' },
54+
{ name: 'sla_due_date', label: 'SLA Due' },
55+
{ name: 'opened_on', label: 'Opened On' },
56+
];
57+
58+
const data = { priority: 'High', sla_due_date: SLA_DUE, opened_on: '2026-10-20' };
59+
60+
/** The chip column (`group flex flex-col …`) whose label is `label`. */
61+
const chip = (label: string) => screen.getByText(label).closest('div.group') as HTMLElement;
62+
63+
describe('a date chip gets the floor its value fits in (objectstack#25897)', () => {
64+
it('gives a `datetime` chip the 16rem floor, the grow and the content cap', () => {
65+
render(<HeaderHighlight fields={fields} data={data} objectSchema={caseSchema} />);
66+
67+
const due = chip('SLA Due');
68+
expect(due).toHaveClass('grow', 'max-w-max', 'basis-[16rem]', 'min-w-[min(16rem,100%)]');
69+
expect(due).not.toHaveClass('basis-[9rem]');
70+
});
71+
72+
it('gives a `date` chip the same floor', () => {
73+
render(<HeaderHighlight fields={fields} data={data} objectSchema={caseSchema} />);
74+
75+
const opened = chip('Opened On');
76+
expect(opened).toHaveClass('basis-[16rem]', 'min-w-[min(16rem,100%)]');
77+
expect(opened).not.toHaveClass('basis-[9rem]');
78+
});
79+
80+
it('CONTROL: a plain `text` chip keeps the 9rem floor', () => {
81+
render(<HeaderHighlight fields={fields} data={data} objectSchema={caseSchema} />);
82+
83+
const priority = chip('Priority');
84+
expect(priority).toHaveClass('basis-[9rem]', 'min-w-[min(9rem,100%)]');
85+
expect(priority).not.toHaveClass('basis-[16rem]');
86+
});
87+
88+
it('keeps the declared truncation for a line that runs out, with the whole value reachable', () => {
89+
render(<HeaderHighlight fields={fields} data={data} objectSchema={caseSchema} />);
90+
91+
const clipBox = chip('SLA Due').querySelector('span.truncate');
92+
expect(clipBox).not.toBeNull();
93+
expect(clipBox).toHaveAttribute('title', SLA_DUE);
94+
});
95+
});
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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#25897 — `record:details` READ mode wraps a plain `text` value
11+
* instead of cutting it to one line.
12+
*
13+
* ## The defect
14+
*
15+
* A HotCRM case read its subject as "Data export timing out for large…" in a
16+
* two-column section with room to its right. The row resolved `text` through
17+
* `getCellRenderer`, whose table maps it to `TextCellRenderer` — the GRID's
18+
* one-line cell, `TruncatedText` (`truncate`: no wrap, an ellipsis). The ruling
19+
* (triage on the card): "Read mode wraps text-like values; truncation only
20+
* where a layout declares it, with the full value reachable."
21+
*
22+
* ## What is pinned, and how
23+
*
24+
* The row now draws every value the table sends to the text cell through
25+
* `TextField`'s own read display, the move objectui#11577 made for `textarea`.
26+
* The value cell is compared, as markup, with what `TextField` renders
27+
* read-only for the same value, and no element between the drawn text and the
28+
* row may carry a utility that stops it wrapping. happy-dom applies no
29+
* Tailwind, so the class list is named here only for that negative check.
30+
*
31+
* Controls:
32+
* - the GRID keeps its one-line cell: `getCellRenderer('text')` is still
33+
* `TextCellRenderer`, which still truncates with the whole value as its
34+
* `title` (truncation where the layout declares it);
35+
* - a host that registered its own `text` face keeps it on the record page.
36+
*/
37+
38+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
39+
import { render, screen, cleanup, act, waitFor } from '@testing-library/react';
40+
import * as React from 'react';
41+
import { RecordContextProvider } from '@object-ui/react';
42+
import { TextField, TextCellRenderer, getCellRenderer, registerFieldRenderer } from '@object-ui/fields';
43+
import { RecordDetailsRenderer } from '../record-details';
44+
45+
/** The case subject the browser pass read cut. */
46+
const SUBJECT = 'Data export timing out for large datasets';
47+
48+
const DESKTOP_WIDTH = 1280;
49+
const MOBILE_WIDTH = 375;
50+
const pinViewport = (width: number) => () => {
51+
Object.defineProperty(window, 'innerWidth', { configurable: true, value: width });
52+
};
53+
54+
const caseSchema = {
55+
name: 'support_case',
56+
label: 'Case',
57+
nameField: 'case_number',
58+
fields: {
59+
case_number: { type: 'text', label: 'Case Number' },
60+
subject: { type: 'text', label: 'Subject' },
61+
reference: { type: 'auto_number', label: 'Reference' },
62+
},
63+
};
64+
const BODY_FIELDS = ['subject', 'reference'];
65+
66+
async function renderBody(record: Record<string, unknown>) {
67+
const ds = { getObjectSchema: vi.fn(async () => caseSchema) };
68+
render(
69+
<RecordContextProvider
70+
objectName="support_case"
71+
recordId="C2"
72+
data={{ id: 'C2', case_number: 'CASE-00002', ...record }}
73+
objectSchema={caseSchema}
74+
dataSource={ds as never}
75+
>
76+
<RecordDetailsRenderer schema={{ fields: BODY_FIELDS } as never} />
77+
</RecordContextProvider>,
78+
);
79+
await waitFor(() => expect(ds.getObjectSchema).toHaveBeenCalled());
80+
await act(async () => {
81+
await ds.getObjectSchema.mock.results[0].value;
82+
});
83+
}
84+
85+
function markupOf(element: React.ReactElement): string {
86+
const { container, unmount } = render(element);
87+
const html = container.innerHTML;
88+
unmount();
89+
return html;
90+
}
91+
92+
const textReadDisplay = (name: string, type: string, value: string) =>
93+
markupOf(<TextField field={{ name, type, label: name } as never} value={value} onChange={() => {}} readonly />);
94+
95+
/** The element holding a row's drawn value, found from the label the row prints. */
96+
function valueCellOf(label: string, layout: 'desktop' | 'mobile'): HTMLElement {
97+
const box = screen.getByText(label).nextElementSibling as HTMLElement | null;
98+
expect(box).not.toBeNull();
99+
if (layout === 'mobile') return box!;
100+
expect(box!.firstElementChild).not.toBeNull();
101+
return box!.firstElementChild as HTMLElement;
102+
}
103+
104+
/** The innermost element whose text is exactly the value. */
105+
function drawingNodeOf(cell: HTMLElement, value: string): HTMLElement {
106+
const hits = [cell, ...Array.from(cell.querySelectorAll<HTMLElement>('*'))].filter(
107+
(el) => el.textContent === value && !Array.from(el.children).some((c) => c.textContent === value),
108+
);
109+
expect(hits).toHaveLength(1);
110+
return hits[0];
111+
}
112+
113+
/** Utilities that keep a value on one line. */
114+
const ONE_LINE = ['truncate', 'whitespace-nowrap', 'text-ellipsis', 'line-clamp-1'];
115+
116+
function oneLineClassesBetween(node: HTMLElement, stop: HTMLElement): string[] {
117+
const found: string[] = [];
118+
for (let el: HTMLElement | null = node; el; el = el.parentElement) {
119+
for (const cls of ONE_LINE) if (el.classList.contains(cls)) found.push(cls);
120+
if (el === stop) break;
121+
}
122+
return found;
123+
}
124+
125+
beforeEach(() => {
126+
// `useRecordEditable` probes `POST /api/v1/security/explain`; happy-dom would
127+
// send that to a real socket, which the repo's network-escape guard fails the
128+
// file for (objectui#6640). Its answer is orthogonal to the read display.
129+
vi.stubGlobal('fetch', vi.fn(async () => ({
130+
ok: true,
131+
status: 200,
132+
json: async () => ({ allowed: true }),
133+
text: async () => '{"allowed":true}',
134+
})) as never);
135+
});
136+
137+
afterEach(() => {
138+
cleanup();
139+
vi.unstubAllGlobals();
140+
});
141+
142+
for (const layout of ['desktop', 'mobile'] as const) {
143+
describe(`objectstack#25897 — record:details read mode, ${layout}: a text value wraps`, () => {
144+
beforeEach(pinViewport(layout === 'desktop' ? DESKTOP_WIDTH : MOBILE_WIDTH));
145+
146+
it('a `text` cell holds exactly the read display TextField renders for it', async () => {
147+
await renderBody({ subject: SUBJECT });
148+
expect(valueCellOf('Subject', layout).innerHTML).toBe(textReadDisplay('subject', 'text', SUBJECT));
149+
});
150+
151+
it('nothing between the drawn subject and the row keeps it on one line', async () => {
152+
await renderBody({ subject: SUBJECT });
153+
const cell = valueCellOf('Subject', layout);
154+
const node = drawingNodeOf(cell, SUBJECT);
155+
expect(node.textContent).toBe(SUBJECT);
156+
expect(oneLineClassesBetween(node, cell)).toEqual([]);
157+
});
158+
});
159+
}
160+
161+
describe('objectstack#25897 — every type the table sends to the text cell reads the same way', () => {
162+
beforeEach(pinViewport(DESKTOP_WIDTH));
163+
164+
it('an `auto_number` value reads through TextField too', async () => {
165+
await renderBody({ subject: SUBJECT, reference: 'REF-000123' });
166+
const cell = valueCellOf('Reference', 'desktop');
167+
expect(cell.innerHTML).toBe(textReadDisplay('reference', 'auto_number', 'REF-000123'));
168+
expect(oneLineClassesBetween(drawingNodeOf(cell, 'REF-000123'), cell)).toEqual([]);
169+
});
170+
});
171+
172+
describe('objectstack#25897 — CONTROLS', () => {
173+
beforeEach(pinViewport(DESKTOP_WIDTH));
174+
175+
it('the grid keeps its one-line text cell, titled with the whole value', () => {
176+
expect(getCellRenderer('text')).toBe(TextCellRenderer);
177+
const { container } = render(
178+
<TextCellRenderer value={SUBJECT} field={{ name: 'subject', type: 'text' } as never} />,
179+
);
180+
const span = container.querySelector('span');
181+
expect(span).toHaveClass('truncate');
182+
expect(span).toHaveAttribute('title', SUBJECT);
183+
});
184+
185+
it("a host's own `text` face is kept on the record page", async () => {
186+
const shipped = getCellRenderer('text');
187+
const HostText = ({ value }: { value: unknown }) => <em data-host-text>{String(value)}</em>;
188+
try {
189+
registerFieldRenderer('text', HostText as never);
190+
await renderBody({ subject: SUBJECT });
191+
const cell = valueCellOf('Subject', 'desktop');
192+
expect(cell.querySelector('em[data-host-text]')?.textContent).toBe(SUBJECT);
193+
} finally {
194+
registerFieldRenderer('text', shipped);
195+
}
196+
});
197+
});

0 commit comments

Comments
 (0)