diff --git a/artifacts/tx-os/src/locales/ar.json b/artifacts/tx-os/src/locales/ar.json
index 692943db..cb09559e 100644
--- a/artifacts/tx-os/src/locales/ar.json
+++ b/artifacts/tx-os/src/locales/ar.json
@@ -964,8 +964,6 @@
"timeOrderError": "وقت النهاية قبل وقت البداية",
"addAttendee": "أضف حاضر",
"addSubheading": "+ عنوان فرعي",
- "addPersonHere": "+ شخص هنا",
- "addPersonHereAria": "إضافة شخص في نهاية هذا القسم",
"addVirtualAttendee": "+ حاضر عبر الاتصال المرئي",
"addInternalAttendee": "+ حاضر داخلي",
"addExternalAttendee": "+ حاضر خارجي",
@@ -1075,8 +1073,6 @@
"moveUp": "تحريك للأعلى",
"moveDown": "تحريك للأسفل",
"dragHandle": "اسحب لإعادة الترتيب",
- "addPersonHere": "إضافة شخص هنا",
- "addPersonHereAria": "إضافة شخص في نهاية هذا القسم",
"removeAll": "حذف جميع الحضور",
"removeAllConfirm": "حذف جميع الحضور ({{count}})؟\n\nسيتم تطبيق التغيير عند حفظ الاجتماع."
},
diff --git a/artifacts/tx-os/src/locales/en.json b/artifacts/tx-os/src/locales/en.json
index d0c48439..50b6b8c6 100644
--- a/artifacts/tx-os/src/locales/en.json
+++ b/artifacts/tx-os/src/locales/en.json
@@ -881,8 +881,6 @@
"timeOrderError": "End time is before start time",
"addAttendee": "Add attendee",
"addSubheading": "+ subheading",
- "addPersonHere": "+ person here",
- "addPersonHereAria": "Add a person at the end of this section",
"addVirtualAttendee": "+ Virtual",
"addInternalAttendee": "+ Internal",
"addExternalAttendee": "+ External",
@@ -976,8 +974,6 @@
"moveUp": "Move up",
"moveDown": "Move down",
"dragHandle": "Drag to reorder",
- "addPersonHere": "Add person here",
- "addPersonHereAria": "Add a person at the end of this section",
"removeAll": "Remove all attendees",
"removeAllConfirm": "Remove all {{count}} attendees?\n\nThe change applies when you save the meeting."
},
diff --git a/artifacts/tx-os/src/pages/executive-meetings.tsx b/artifacts/tx-os/src/pages/executive-meetings.tsx
index 870011f2..586b61f6 100644
--- a/artifacts/tx-os/src/pages/executive-meetings.tsx
+++ b/artifacts/tx-os/src/pages/executive-meetings.tsx
@@ -3208,10 +3208,6 @@ function AttendeesCell({
onCancelAdd: cancelAdd,
addLabel: t("executiveMeetings.schedule.addAttendee"),
addSubheadingLabel: t("executiveMeetings.schedule.addSubheading"),
- addPersonHereLabel: t("executiveMeetings.schedule.addPersonHere"),
- addPersonHereAriaLabel: t(
- "executiveMeetings.schedule.addPersonHereAria",
- ),
editAriaLabel: t("executiveMeetings.schedule.editAttendeeAria"),
newAriaLabel: t("executiveMeetings.schedule.newAttendeeAria"),
editSubheadingAriaLabel: t(
@@ -3372,10 +3368,6 @@ type AttendeeFlowSharedProps = {
onCancelAdd?: () => void;
addLabel: string;
addSubheadingLabel: string;
- // Inline "+ person here" chip rendered right after each subheading row
- // so the user can grow a section without scrolling to the trailing "+".
- addPersonHereLabel: string;
- addPersonHereAriaLabel: string;
editAriaLabel: string;
newAriaLabel: string;
editSubheadingAriaLabel: string;
@@ -3421,8 +3413,6 @@ function AttendeeFlow({
onCancelAdd,
addLabel,
addSubheadingLabel,
- addPersonHereLabel,
- addPersonHereAriaLabel,
editAriaLabel,
newAriaLabel,
editSubheadingAriaLabel,
@@ -3430,14 +3420,13 @@ function AttendeeFlow({
}: { items: AttendeeWithIndex[] } & AttendeeFlowSharedProps) {
const editable = canMutate && !!onSaveAttendeeName;
const showAdd = canMutate && !!onStartAdd && !isPending;
- // True only when the in-flight ghost is targeted at a specific slot
- // (set by clicking a per-subheading "+ person here" chip). When unset
- // we fall back to the legacy trailing render so existing screenshots
- // and the trailing "+" / "+ subheading" buttons keep working.
- const hasInlineInsert =
- isPending &&
- pendingKind === "person" &&
- pendingInsertAtIndex !== undefined;
+ // True when the in-flight ghost is targeted at a specific slot — set
+ // by clicking either the top "+ عنوان فرعي" chip or the after-section
+ // "+ عنوان فرعي" chip. The same flag drives inline rendering for the
+ // person ghost (still wired through the manage dialog flow). When
+ // unset we fall back to the legacy trailing render so the trailing
+ // "+" / "+ subheading" buttons keep working.
+ const hasInlineInsert = isPending && pendingInsertAtIndex !== undefined;
// Each subheading starts a fresh numbered section. Within a section,
// persons are numbered 1..N starting over after every subheading.
@@ -3502,6 +3491,41 @@ function AttendeeFlow({
}
}
+ // The pending subheading ghost
. Used for inline rendering when
+ // the user clicks the top "+ عنوان فرعي" chip or an after-section
+ // chip — the ghost materializes at the requested slot rather than the
+ // trailing fallback so the user sees their click land where expected.
+ const renderPendingSubheadingLi = (key: string) => {
+ if (
+ !(canMutate && isPending && pendingKind === "subheading" && onCommitAdd)
+ ) {
+ return null;
+ }
+ return (
+
. Defined once so we can render it
// either inline (at the slot the user clicked) or as a trailing row
// (legacy "+" button at the end of the cell). It always shows the next
@@ -3564,12 +3588,44 @@ function AttendeeFlow({
return (
+ {/* Top "+ عنوان فرعي" chip — lets the user start the cell with a
+ labeled section, slotted BEFORE the first existing item. We
+ only render it when the cell already has at least one item;
+ for empty cells the trailing "+ subheading" button suffices. */}
+ {showAdd && items.length > 0 && (
+
+
+
+ )}
{items.flatMap(({ a, i }, listIdx) => {
const isSub = (a.kind ?? "person") === "subheading";
const nodes: ReactNode[] = [];
if (inlineGhostTargetListIdx === listIdx) {
- const ghost = renderPendingPersonLi(`pending-inline-${pendingKey}`);
- if (ghost) nodes.push(ghost);
+ if (pendingKind === "subheading") {
+ const ghost = renderPendingSubheadingLi(
+ `pending-inline-sub-${pendingKey}`,
+ );
+ if (ghost) nodes.push(ghost);
+ } else {
+ const ghost = renderPendingPersonLi(
+ `pending-inline-${pendingKey}`,
+ );
+ if (ghost) nodes.push(ghost);
+ }
}
if (isSub) {
// Subheading row: full-width line break inside the flex-wrap so
@@ -3601,39 +3657,6 @@ function AttendeeFlow({
)}
,
);
- // Subheading-only section edge case: if the very next item in
- // the list is ALSO a subheading (or this subheading is the
- // last item with no person after it), the section it just
- // started has zero persons. Render a chip right after the
- // subheading so the user can still grow that empty section
- // without opening the manage dialog. The chip's insert index
- // is the slot immediately after this subheading's global
- // attendees index.
- const nextItem = items[listIdx + 1];
- const sectionIsEmpty =
- !nextItem ||
- (nextItem.a.kind ?? "person") === "subheading";
- if (showAdd && sectionIsEmpty) {
- nodes.push(
-
-
-
,
- );
- }
return nodes;
}
// Person row — section-local 0-based index pre-computed above.
@@ -3683,71 +3706,61 @@ function AttendeeFlow({
)}
,
);
- // Section-tail "+ شخص هنا" chip — appears AFTER the last person
- // of each section (a section ends right before the next
- // subheading, or at end-of-list). The chip's insertAtIndex is
- // the slot immediately after this person's global attendees
- // index — so the new row lands at the end of THIS section, not
- // at the start of the next one. We only render this when the
- // cell already has at least one subheading: in a no-subheading
- // cell the trailing global "+" already covers the same slot.
+ // After-section "+ عنوان فرعي" chip — appears AFTER the last
+ // person of each section that is followed by ANOTHER section
+ // (i.e. the next item is a subheading). This lets the user
+ // insert a new section heading at the boundary between two
+ // existing sections. The trailing section is intentionally
+ // skipped: the trailing "+ عنوان فرعي" button below already
+ // covers that slot, so duplicating the chip would be noise.
+ // The chip's insertAtIndex is the slot immediately after this
+ // person's global attendees index, so the new subheading lands
+ // BEFORE the existing next-section subheading.
const nextItemForPerson = items[listIdx + 1];
- const isLastInSection =
- !nextItemForPerson ||
+ const nextIsSubheading =
+ nextItemForPerson &&
(nextItemForPerson.a.kind ?? "person") === "subheading";
- if (showAdd && hasAnySubheading && isLastInSection) {
+ if (showAdd && nextIsSubheading) {
nodes.push(
,
);
}
return nodes;
})}
- {/* Inline ghost pinned to the trailing slot (insert-at-end case). */}
+ {/* Inline ghost pinned to the trailing slot (insert-at-end case).
+ Either kind may land here: a person from manage flows, or a
+ subheading from the trailing "+ عنوان فرعي" button when the
+ insertAtIndex equals items.length. */}
{hasInlineInsert &&
inlineGhostTargetListIdx === null &&
+ pendingKind === "person" &&
renderPendingPersonLi(`pending-inline-trailing-${pendingKey}`)}
- {/* Legacy trailing ghost (no inline insert in flight). */}
+ {hasInlineInsert &&
+ inlineGhostTargetListIdx === null &&
+ pendingKind === "subheading" &&
+ renderPendingSubheadingLi(`pending-inline-trailing-sub-${pendingKey}`)}
+ {/* Legacy trailing ghost (no inline insert in flight) — used for
+ the global trailing "+" / "+ subheading" buttons. */}
{!hasInlineInsert &&
renderPendingPersonLi(`pending-trailing-${pendingKey}`)}
- {canMutate &&
- isPending &&
- pendingKind === "subheading" &&
- onCommitAdd && (
-
@@ -4332,35 +4345,6 @@ function MeetingFormDialog({
],
});
}
- // Insert a fresh person row at a specific slot (used by the per-section
- // "+ شخص هنا" buttons rendered after each subheading inside the manage
- // dialog). Falls back to appending when the index is out of range.
- function insertAttendeeAt(insertAtIndex: number) {
- const safeIdx = Math.max(
- 0,
- Math.min(insertAtIndex, state.attendees.length),
- );
- const newRow: Attendee = {
- name: "",
- title: null,
- // Inherit the previous row's attendance type so the inserted row
- // visually belongs to the same section the user clicked into.
- attendanceType:
- safeIdx > 0
- ? state.attendees[safeIdx - 1].attendanceType
- : "internal",
- sortOrder: safeIdx,
- kind: "person",
- _sid: genAttendeeSid(),
- };
- const arr = state.attendees.slice();
- arr.splice(safeIdx, 0, newRow);
- // Renumber sortOrder so the array stays a contiguous 0..N-1 sequence.
- arr.forEach((row, idx) => {
- row.sortOrder = idx;
- });
- onChange({ ...state, attendees: arr });
- }
function addSubheading() {
onChange({
...state,
@@ -4623,57 +4607,25 @@ function MeetingFormDialog({
items={sortableIds}
strategy={verticalListSortingStrategy}
>
- {state.attendees.flatMap((a, i) => {
- const nodes: ReactNode[] = [
- moveAttendee(i, -1)}
- onMoveDown={() => moveAttendee(i, 1)}
- onChangeName={(v) => setAttendee(i, { name: v })}
- onChangeTitle={(v) =>
- setAttendee(i, { title: v || null })
- }
- onChangeAttendanceType={(v) =>
- setAttendee(i, { attendanceType: v })
- }
- onRemove={() => removeAttendee(i)}
- />,
- ];
- // Per-section "+ شخص هنا" button: appears AFTER every
- // subheading row so the user can grow the section it
- // starts without scrolling to the bottom of the list
- // and then dragging the new row up. Insertion target
- // is the slot right after the subheading (i + 1).
- if ((a.kind ?? "person") === "subheading") {
- nodes.push(
-
-
-
,
- );
- }
- return nodes;
- })}
+ {state.attendees.map((a, i) => (
+ moveAttendee(i, -1)}
+ onMoveDown={() => moveAttendee(i, 1)}
+ onChangeName={(v) => setAttendee(i, { name: v })}
+ onChangeTitle={(v) =>
+ setAttendee(i, { title: v || null })
+ }
+ onChangeAttendanceType={(v) =>
+ setAttendee(i, { attendanceType: v })
+ }
+ onRemove={() => removeAttendee(i)}
+ />
+ ))}
{state.attendees.length === 0 && (
diff --git a/artifacts/tx-os/tests/executive-meetings-attendee-insert-reorder.spec.mjs b/artifacts/tx-os/tests/executive-meetings-attendee-insert-reorder.spec.mjs
index bd4532a0..73f051df 100644
--- a/artifacts/tx-os/tests/executive-meetings-attendee-insert-reorder.spec.mjs
+++ b/artifacts/tx-os/tests/executive-meetings-attendee-insert-reorder.spec.mjs
@@ -1,21 +1,24 @@
import { test, expect } from "@playwright/test";
import pg from "pg";
-// E2E coverage for Task #220 — "Insert persons mid-list and drag-reorder
-// attendees" inside executive-meetings.
+// E2E coverage for Task #227 — "Restructure attendee cell controls
+// around section headings" inside executive-meetings.
//
// What we verify:
-// 1. Per-section "+ شخص هنا" chip on the schedule grid: the chip is
-// rendered AFTER THE LAST PERSON of each section (a section ends
-// right before the next subheading or at end-of-list). Clicking it
-// opens a ghost row at the slot right after that last person — i.e.
-// at the TAIL of the section the chip belongs to — and committing
-// the typed name persists the new attendee at that exact slot,
-// pushing the next subheading down by one.
-// 2. Drag-and-drop reordering inside the manage dialog: picking up a
+// 1. Top "+ عنوان فرعي" chip on the schedule grid: the chip is
+// rendered BEFORE the first attendee of each AttendeeFlow group.
+// Clicking it opens a pending subheading ghost at slot 0 (or the
+// group's first global index) and committing the typed text
+// persists the new subheading at the very top of the cell.
+// 2. After-section "+ عنوان فرعي" chip: rendered after the last
+// person of each section that is followed by ANOTHER section
+// (i.e. a subheading comes next). Clicking it inserts a new
+// subheading at the boundary, BEFORE the existing next-section
+// subheading.
+// 3. Drag-and-drop reordering inside the manage dialog: picking up a
// row by its grip handle and moving it down with the keyboard
// reorders the attendee array, and saving persists the new order.
-// Both scenarios run in BOTH locales so the RTL/LTR mirror behaves the
+// All scenarios run in BOTH locales so the RTL/LTR mirror behaves the
// same way.
const DATABASE_URL = process.env.DATABASE_URL;
@@ -164,24 +167,21 @@ const langOffsets2 = { en: 3, ar: 4 };
const langOffsets3 = { en: 5, ar: 6 };
for (const lang of ["en", "ar"]) {
- test(`Schedule [${lang}]: "+ person here" chip after the last person of a section inserts at the section tail`, async ({
+ test(`Schedule [${lang}]: top "+ subheading" chip inserts a subheading at slot 0 of the cell`, async ({
page,
}) => {
const date = uniqueFutureDate(langOffsets[lang]);
const meetingId = await insertMeeting({
meetingDate: date,
dailyNumber: 1,
- titleEn: `Insert Mid Section ${lang} ${Date.now().toString(36)}`,
- titleAr: `إدراج وسط القسم ${lang} ${Date.now().toString(36)}`,
+ titleEn: `Top Subheading ${lang} ${Date.now().toString(36)}`,
+ titleAr: `عنوان فرعي علوي ${lang} ${Date.now().toString(36)}`,
startTime: "09:00:00",
endTime: "10:00:00",
});
- // Layout: [Alpha, Beta, Sec2Header, Gamma].
- // Section 1 (implicit) = [Alpha, Beta] ← chip after Beta (idx 1)
- // Section 2 = [Gamma] ← chip after Gamma (idx 3)
- // The user clicks the chip at idx 1 — the chip belongs to the
- // SECTION ABOVE the subheading. Insertion target is idx 2, so the
- // new row lands BEFORE the subheading.
+ // Seed: two internal persons, no subheadings. The top chip should
+ // render BEFORE PersonAlpha and click-to-insert at attendees idx 0,
+ // pushing both persons down by one slot.
await insertAttendee({
meetingId,
name: "PersonAlpha",
@@ -196,20 +196,6 @@ for (const lang of ["en", "ar"]) {
sortOrder: 1,
kind: "person",
});
- await insertAttendee({
- meetingId,
- name: "Sec2Header-Q7",
- attendanceType: "internal",
- sortOrder: 2,
- kind: "subheading",
- });
- await insertAttendee({
- meetingId,
- name: "PersonGamma",
- attendanceType: "internal",
- sortOrder: 3,
- kind: "person",
- });
await setAdminPreferredLanguage(lang);
try {
@@ -224,60 +210,168 @@ for (const lang of ["en", "ar"]) {
await expect(row).toBeVisible({ timeout: 15_000 });
await ensureEditMode(page);
- // The "+ person here" chip is rendered AFTER each section's last
- // person row, indexed by that person's global attendees idx.
- // Section 1 ends at PersonBeta (idx 1) → chip testid -1.
- // The chip on the subheading row itself should NOT exist (the
- // section above it is non-empty).
- const chip = row.getByTestId("em-add-person-here-1");
- await expect(chip).toBeVisible({ timeout: 10_000 });
- await expect(row.getByTestId("em-add-person-here-2")).toHaveCount(0);
- await chip.click();
+ // Top "+ subheading" chip lives at the very top of the
+ // AttendeeFlow group, addType = internal here.
+ const topChip = row.getByTestId("em-add-subheading-top-internal");
+ await expect(topChip).toBeVisible({ timeout: 10_000 });
+ await topChip.click();
- // The pending ghost
appears, with a Tiptap editor that has
- // started in edit mode. Type the new name and blur to commit.
- const pending = row.getByTestId("em-add-attendee-pending-internal");
+ // The pending subheading ghost
appears with a Tiptap editor.
+ const pending = row.getByTestId("em-add-subheading-pending-internal");
await expect(pending).toBeVisible({ timeout: 5_000 });
const editor = pending.locator('[contenteditable="true"]').first();
await expect(editor).toBeVisible({ timeout: 5_000 });
await editor.click();
- await editor.fill("InsertedDelta");
- // Blur the editor (forceSaveOnBlur fires the commit) by clicking
- // the meeting title cell, which is unrelated to the attendee
- // input and won't open another editor.
+ await editor.fill("TopSection");
+ // Blur to commit by clicking somewhere unrelated.
await page.locator("body").click({ position: { x: 5, y: 5 } });
- // Wait for the round-trip: the cell should now show 4 person
- // rows and 1 subheading.
+ // After the round-trip the cell should show 2 person rows + 1
+ // subheading.
await expect(
row.locator('[data-testid^="em-attendee-row-"]'),
- ).toHaveCount(4, { timeout: 10_000 });
+ ).toHaveCount(2, { timeout: 10_000 });
await expect(
row.locator('[data-testid^="em-attendee-subheading-"]'),
).toHaveCount(1);
- // Verify the persisted order via DB — the new person must land
- // BEFORE the subheading (at the tail of section 1), not after.
const persisted = await getAttendeesOrdered(meetingId);
const stripTags = (s) => s.replace(/<[^>]+>/g, "").trim();
const orderedNames = persisted.map((r) => stripTags(r.name));
expect(orderedNames).toEqual([
+ "TopSection",
"PersonAlpha",
"PersonBeta",
- "InsertedDelta",
- "Sec2Header-Q7",
+ ]);
+ expect(persisted.map((r) => r.kind)).toEqual([
+ "subheading",
+ "person",
+ "person",
+ ]);
+ expect(persisted.map((r) => r.sort_order)).toEqual([0, 1, 2]);
+ } finally {
+ if (originalAdminPreferredLanguage) {
+ await setAdminPreferredLanguage(originalAdminPreferredLanguage).catch(
+ () => {
+ /* best-effort; afterAll also restores */
+ },
+ );
+ }
+ }
+ });
+
+ test(`Schedule [${lang}]: after-section "+ subheading" chip inserts a subheading between two existing sections`, async ({
+ page,
+ }) => {
+ const date = uniqueFutureDate(langOffsets[lang] + 100);
+ const meetingId = await insertMeeting({
+ meetingDate: date,
+ dailyNumber: 2,
+ titleEn: `Mid Subheading ${lang} ${Date.now().toString(36)}`,
+ titleAr: `عنوان فرعي وسطي ${lang} ${Date.now().toString(36)}`,
+ startTime: "09:00:00",
+ endTime: "10:00:00",
+ });
+ // Seed: [Sec1Header, Alpha, Beta, Sec2Header, Gamma]. The
+ // after-section chip is rendered after the last person of each
+ // section that has another section right after it. Beta sits at
+ // attendees idx 2 with Sec2Header at idx 3 → chip testid is
+ // `em-add-subheading-after-2`. The trailing section (Gamma) has
+ // nothing after it → no after-section chip there.
+ await insertAttendee({
+ meetingId,
+ name: "Sec1Header",
+ attendanceType: "internal",
+ sortOrder: 0,
+ kind: "subheading",
+ });
+ await insertAttendee({
+ meetingId,
+ name: "PersonAlpha",
+ attendanceType: "internal",
+ sortOrder: 1,
+ kind: "person",
+ });
+ await insertAttendee({
+ meetingId,
+ name: "PersonBeta",
+ attendanceType: "internal",
+ sortOrder: 2,
+ kind: "person",
+ });
+ await insertAttendee({
+ meetingId,
+ name: "Sec2Header",
+ attendanceType: "internal",
+ sortOrder: 3,
+ kind: "subheading",
+ });
+ await insertAttendee({
+ meetingId,
+ name: "PersonGamma",
+ attendanceType: "internal",
+ sortOrder: 4,
+ kind: "person",
+ });
+
+ await setAdminPreferredLanguage(lang);
+ try {
+ await loginViaUi(page);
+ await page.goto("/executive-meetings");
+ await expect(page.locator("html")).toHaveAttribute("lang", lang, {
+ timeout: 5_000,
+ });
+ await page.locator('input[type="date"]').first().fill(date);
+
+ const row = page.getByTestId(`em-row-${meetingId}`);
+ await expect(row).toBeVisible({ timeout: 15_000 });
+ await ensureEditMode(page);
+
+ const chip = row.getByTestId("em-add-subheading-after-2");
+ await expect(chip).toBeVisible({ timeout: 10_000 });
+ // The trailing section's last person (Gamma at idx 4) must NOT
+ // have an after-section chip — the trailing "+ subheading"
+ // button below already covers that slot.
+ await expect(row.getByTestId("em-add-subheading-after-4")).toHaveCount(0);
+ await chip.click();
+
+ const pending = row.getByTestId("em-add-subheading-pending-internal");
+ await expect(pending).toBeVisible({ timeout: 5_000 });
+ const editor = pending.locator('[contenteditable="true"]').first();
+ await expect(editor).toBeVisible({ timeout: 5_000 });
+ await editor.click();
+ await editor.fill("MidSection");
+ await page.locator("body").click({ position: { x: 5, y: 5 } });
+
+ await expect(
+ row.locator('[data-testid^="em-attendee-row-"]'),
+ ).toHaveCount(3, { timeout: 10_000 });
+ await expect(
+ row.locator('[data-testid^="em-attendee-subheading-"]'),
+ ).toHaveCount(3);
+
+ const persisted = await getAttendeesOrdered(meetingId);
+ const stripTags = (s) => s.replace(/<[^>]+>/g, "").trim();
+ const orderedNames = persisted.map((r) => stripTags(r.name));
+ // The new subheading must land BEFORE Sec2Header (the existing
+ // next-section subheading), at slot 3.
+ expect(orderedNames).toEqual([
+ "Sec1Header",
+ "PersonAlpha",
+ "PersonBeta",
+ "MidSection",
+ "Sec2Header",
"PersonGamma",
]);
- expect(persisted.map((r) => r.sort_order)).toEqual([0, 1, 2, 3, 4]);
-
- // Numbering: section 1 = [Alpha, Beta, InsertedDelta] → 1-, 2-, 3-;
- // section 2 = [Gamma] → 1- (cell has a subheading so all persons
- // are numbered).
- const indexTexts = await row
- .locator('[data-testid^="em-attendee-index-"]')
- .allInnerTexts();
- const trimmed = indexTexts.map((s) => s.trim().replace(/\s+/g, ""));
- expect(trimmed).toEqual(["1-", "2-", "3-", "1-"]);
+ expect(persisted.map((r) => r.kind)).toEqual([
+ "subheading",
+ "person",
+ "person",
+ "subheading",
+ "subheading",
+ "person",
+ ]);
+ expect(persisted.map((r) => r.sort_order)).toEqual([0, 1, 2, 3, 4, 5]);
} finally {
if (originalAdminPreferredLanguage) {
await setAdminPreferredLanguage(originalAdminPreferredLanguage).catch(