Skip to content

Commit a49d633

Browse files
fix(app-shell): the inbox popover fits a phone, and its controls and the notification switches are 44 px tap targets (#29165)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbVnsesRDUVqMHYYYFpsmioi)_ Fixes #29076 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:手机宽度(`md` 以下)下,收件箱弹层的宽度不再超出屏幕,左右各留 0.5 rem;弹层里的按钮都至少 44 px 高,包括「全部已读」、三个标签页、未读/全部筛选,以及三个「查看全部」类入口。三个标签页在手机上不显示图标,因此能排进一行。账号菜单里的两个通知开关,现在整行都可以点,行高 44 px。桌面尺寸不变。 - **为什么改**:移动端 App 直接嵌入 Console。375 px 的手机上弹层宽 384 px,右侧 9 px 被裁掉,「Activity feed」标签和「Mark read」列看不全。弹层内的按钮只有 16–36 px 高,开关只有 44 × 24。 - **风险与回滚**:低。只改两个组件的 `className`:开关行从 `div` 改成 `label`,弹层加了 `collisionPadding`。不加键、不加 prop、不加文案。回滚即回退本 PR。 - **先看**:`ui/packages/app-shell/src/layout/InboxPopover.tsx` 里的 `PopoverContent`(`max-w-[calc(100vw-1rem)]` 与 `collisionPadding`),以及 `PHONE_TAP` / `TAB_TRIGGER_CLASS`;`ui/packages/app-shell/src/layout/NotificationPreferencesMenu.tsx` 里的 `ROW_CLASS`:开关行改成 `label` 后,点整行就是点开关,被浏览器屏蔽的桌面通知开关仍然点不动。 - **两分钟验证**:`pnpm --dir ui exec vitest run packages/app-shell/src/layout/__tests__/InboxPopover.phoneWidth-29076.test.tsx packages/app-shell/src/layout/__tests__/NotificationPreferencesMenu.phoneWidth-29076.test.tsx`,预期 9 个用例全部通过。 - **需要你做的**:无,席位落地。 Part of #29040. Claim: issuecomment-6571189550, surface amended in issuecomment-6572115270. ## What changes The mobile app hosts the Console in a WebView, so below `md` the Console is the phone UI. This PR covers the Notifications flow: the inbox popover, marking notifications read, opening the target record, and the account menu's notification switches. - **`InboxPopover`**: the popover keeps `w-96` and gains `max-w-[calc(100vw-1rem)]` with `collisionPadding={8}`. At 375 px it is now 359 px wide at x = 8; it was 384 px at x = 0, with its right 9 px cut off. Below `md` (`max-md:` utilities, so desktop is unchanged), every control the popover draws besides the rows gets `min-h-11`. These are "Mark all read", the three tabs, the Unread and All filters, "View all notifications", "View approvals", "Open Approvals Inbox" and "View all activity". "All" also gets `min-w-11`. Below `md` the tabs drop their icons and size by content (`flex-auto`, `px-1.5`), so all three fit the narrower strip. The bell trigger's class is not touched; #29075 owns it. - **`NotificationPreferencesMenu`**: each switch row is now a `label` that wraps its switch. A tap anywhere on the row toggles the switch, and below `md` the row is 44 px tall. The `Switch` primitive (44 × 24) is not touched. A disabled desktop switch stays off whatever is tapped, and turning it on still asks for permission from that tap, the one gesture that may ask. No key, prop, locale string or spec surface is added. ## Tests - `InboxPopover.phoneWidth-29076.test.tsx` (new, real Radix popover, tabs and English pack): the dialog carries the viewport cap and keeps `w-96`. Every listed control carries the phone floor in each tab and state, including both Approvals states. "All" carries its min width. Each tab's icon is hidden below `md`. - `NotificationPreferencesMenu.phoneWidth-29076.test.tsx` (new): each switch sits in a 44 px label row. Tapping the "In-app alerts" text toggles it. Tapping the "Desktop notifications" text asks for permission once. A blocked desktop row is neither asked nor turned on. - Against `origin/main`'s two source files, the new files went 8 failed and 1 passed. The one that passed is the blocked-row guard, which main also satisfies. ## Acceptance notes The card's run record, measured in a real browser. The Console is a production build of `ui/apps/console`, served by `vite preview` and proxied to `objectstack dev --seed-admin --fresh`. The examples are showcase (`com.example.showcase`) and CRM (`com.example.crm`). Chromium ran with `isMobile` and `hasTouch` at 375×812 and 390×844. Showcase's inbox rows come from its shipped `showcase_task_assigned_notify` flow, fired by reassigning seeded tasks. CRM ships no notify flow, so its rows were inserted as `sys_inbox_message` rows that link to `crm_lead` records. Pages: the app's landing page, the open inbox and its three tabs, the target record, the full inbox (`sys_inbox_message?view=mine`) and the account menu. | Check | 375 showcase | 375 CRM | 390 showcase | 390 CRM | | --- | --- | --- | --- | --- | | No horizontal page scroll | pass / pass | pass / pass | pass / pass | pass / pass | | Touch targets ≥ 44 px | **fail** / pass | **fail** / pass | **fail** / pass | **fail** / pass | | Dialogs and drawers fit the viewport | **fail** / pass | **fail** / pass | pass (0 px spare) / pass | pass (0 px spare) / pass | | Virtual keyboard does not cover the focused field | n/a | n/a | n/a | n/a | | Long lists responsive at 4× CPU | pass / pass | pass / pass | pass / pass | pass / pass | | Mark read, open the target record | pass / pass | pass / pass | pass / pass | pass / pass | Each cell reads before / after. - **Touch targets, before → after (375 / 390):** | Control | Before | After | | --- | --- | --- | | Mark all read | 71 × 16 | 87 × 44 | | Tabs | 28 tall | 44 tall | | Unread | 24 tall | 44 tall | | All | 30 × 24 | 44 × 44 | | View all notifications | 16 tall | full width × 44 | | View approvals | 36 tall | 124 × 44 | | Open Approvals Inbox | 16 tall | 146 × 44 | | View all activity | 16 tall | full width × 44 | | In-app alerts / Desktop notifications switches | 44 × 24 | rows 246 × 44 | Rows (293–318 × 75) and the group "Mark read" (64 × 75) already passed. - **Keyboard: not applicable.** No step of this flow focuses a text field. The popover has none, and at phone width the full inbox page shows no search input; its toolbar is the view switcher, Filter and Display. - **4× CPU** (CDP `Emulation.setCPUThrottlingRate`, production build): tap to open the popover, five opens at 375 px. On main this took 298–554 ms (showcase) and 316–662 ms (CRM). On this branch it took 460–627 ms and 288–498 ms. Scrolling the popover's list (up to 20 rows) had a worst frame of 27–85 ms. - **Still failing, carried by other cards:** the bell trigger is 32 × 32. Its class belongs to the top-bar sweep, #29075, and this PR leaves it alone. The full inbox page is the generic list view. Its toolbar has the view switcher at 125 × 24 and Filter and Display at 28 × 28, and in some runs the first scroll frame took 284–495 ms at 4×. That belongs to the Lists card, #29074. - Before and after screenshots of each step at both widths are in the project folder, as the card asks. The seat links them. ## Verification Readings on head `e2c6f5d4` (base `6b3ba82c`). <details><summary>Tests</summary> - Every ui test file that names `InboxPopover` or `NotificationPreferencesMenu` (`git grep -l`, 47 files, including the two new ones and the `AppHeader` and Home suites), under the verify lock with `--maxWorkers=2`: 47 files and 454 tests passed. This is a narrowing; CI's Test UI runs the rest of app-shell. - Ablation (one time, not kept): with `origin/main`'s `InboxPopover.tsx` and `NotificationPreferencesMenu.tsx` copied in, the two new files went 8 failed and 1 passed. The restore was proven by `cmp` against the fix. </details> <details><summary>Type-check and lint</summary> - `pnpm --filter @object-ui/app-shell run type-check` (both `tsc` legs, src and tests): exit 0. - eslint (`--no-inline-config`, from `ui/`) on the four changed source and test files: exit 0. - `pnpm check:new-line-citations` under `ui/`: 0 new citations. </details> <details><summary>Derived gates</summary> `node scripts/pm/dispatch-gates.mjs --ran`: 34 derived families accounted for, 34 run, 0 NOT MEASURED, every one exit 0. That includes `node scripts/check-sdui-manifest.mjs` and its `--self-test`, `pnpm --dir ui exec vitest run scripts/` (169 files passed, 2 skipped; 4900 tests passed) and `pnpm check:type-check-debt` under the verify lock. The PR-body gates (`check-changeset-no-major`, `check-closing-keyword-parity`, `check-adr-0087-registration`, `check-empty-changeset`) ran on this body. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QsR2jS2XRtTSjBZnMRZ9UG --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6dda0ce commit a49d633

5 files changed

Lines changed: 268 additions & 19 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): the inbox popover fits a 375 px phone, and its controls and the account menu's notification switches are 44 px tap targets below `md`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The mobile app hosts the Console, so below `md` these are phone screens.
11+
12+
- **The inbox popover fits.** It stays `w-96` on desktop, but is never wider than the viewport less 0.5 rem on each side, and keeps that gutter from the edges. At 375 px it was 384 px wide and its right 9 px were cut off, which hid the Activity tab, "Mark all read" and the per-group "Mark read".
13+
- **Its controls are 44 px tall below `md`:** "Mark all read", the three tabs, the Unread and All filters, "View all notifications", "View approvals", "Open Approvals Inbox" and "View all activity". They measured 16 to 36 px. Below `md` the tabs also drop their icons and share the strip by content width, so all three fit.
14+
- **Each notification switch in the account menu is now the control of its whole row.** The rows are "In-app alerts" and "Desktop notifications". A tap anywhere on the row toggles the switch, and below `md` the row is 44 px tall. The switch alone is 44 × 24. A blocked desktop switch stays off whatever is tapped, and turning it on still asks for permission from that tap.
15+
16+
Desktop sizes do not change.

‎ui/packages/app-shell/src/layout/InboxPopover.tsx‎

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,18 @@ function openingTab(hasItems: Record<InboxTab, boolean>, picked: InboxTab | null
105105
return INBOX_TABS.find((t) => hasItems[t]) ?? picked ?? 'notifications';
106106
}
107107

108+
/**
109+
* The phone floor for a tap target: 44 px tall below `md`, the width at which
110+
* the Console is the phone app's UI. Desktop sizes are untouched.
111+
*/
112+
const PHONE_TAP = 'max-md:min-h-11';
113+
114+
/**
115+
* Below `md` the three tabs share the strip by content width with their icons
116+
* dropped, so the third tab is not pushed past a 359 px popover.
117+
*/
118+
const TAB_TRIGGER_CLASS = `text-xs gap-1.5 data-[state=active]:bg-transparent ${PHONE_TAP} max-md:flex-auto max-md:px-1.5`;
119+
108120
export interface InboxPopoverProps {
109121
notifications: InboxNotification[];
110122
unreadCount: number;
@@ -384,7 +396,14 @@ export function InboxPopover({
384396
)}
385397
</Button>
386398
</PopoverTrigger>
387-
<PopoverContent align="end" sideOffset={8} className="w-96 p-0">
399+
{/* Never wider than the viewport less a 0.5 rem gutter on each side, and
400+
kept that far from its edges: a 375 px phone is narrower than w-96. */}
401+
<PopoverContent
402+
align="end"
403+
sideOffset={8}
404+
collisionPadding={8}
405+
className="w-96 max-w-[calc(100vw-1rem)] p-0"
406+
>
388407
<div className="flex items-center justify-between px-3 pt-3 pb-1">
389408
<div className="text-sm font-semibold">
390409
{t('sidebar.inbox', { defaultValue: 'Inbox' })}
@@ -393,7 +412,7 @@ export function InboxPopover({
393412
<button
394413
type="button"
395414
onClick={onMarkAllRead}
396-
className="text-xs text-muted-foreground hover:text-foreground"
415+
className={`text-xs text-muted-foreground hover:text-foreground ${PHONE_TAP} max-md:px-2`}
397416
>
398417
{t('notifications.markAllRead', { defaultValue: 'Mark all read' })}
399418
</button>
@@ -438,9 +457,9 @@ export function InboxPopover({
438457
</div>
439458
)}
440459
<Tabs value={shownTab} onValueChange={handleTabChange} className="w-full">
441-
<TabsList className="w-full justify-start rounded-none border-b bg-transparent px-1 h-9">
442-
<TabsTrigger value="notifications" className="text-xs gap-1.5 data-[state=active]:bg-transparent">
443-
<Bell className="h-3.5 w-3.5" />
460+
<TabsList className="w-full justify-start rounded-none border-b bg-transparent px-1 h-9 max-md:h-auto">
461+
<TabsTrigger value="notifications" className={TAB_TRIGGER_CLASS}>
462+
<Bell className="h-3.5 w-3.5 max-md:hidden" aria-hidden />
444463
{t('sidebar.notifications', { defaultValue: 'Notifications' })}
445464
{unreadTopics > 0 && (
446465
<span
@@ -451,8 +470,8 @@ export function InboxPopover({
451470
</span>
452471
)}
453472
</TabsTrigger>
454-
<TabsTrigger value="approvals" className="text-xs gap-1.5 data-[state=active]:bg-transparent">
455-
<CheckSquare className="h-3.5 w-3.5" />
473+
<TabsTrigger value="approvals" className={TAB_TRIGGER_CLASS}>
474+
<CheckSquare className="h-3.5 w-3.5 max-md:hidden" aria-hidden />
456475
{t('sidebar.approvals', { defaultValue: 'Approvals' })}
457476
{pendingApprovalsCount > 0 && (
458477
<span
@@ -464,8 +483,8 @@ export function InboxPopover({
464483
)}
465484
</TabsTrigger>
466485
{showActivity && (
467-
<TabsTrigger value="activity" className="text-xs gap-1.5 data-[state=active]:bg-transparent">
468-
<ActivityIcon className="h-3.5 w-3.5" />
486+
<TabsTrigger value="activity" className={TAB_TRIGGER_CLASS}>
487+
<ActivityIcon className="h-3.5 w-3.5 max-md:hidden" aria-hidden />
469488
{t('sidebar.activityFeed', { defaultValue: 'Activity feed' })}
470489
</TabsTrigger>
471490
)}
@@ -479,7 +498,7 @@ export function InboxPopover({
479498
<button
480499
type="button"
481500
onClick={() => setNotifFilter('unread')}
482-
className={`text-xs px-2 py-1 rounded-md transition-colors ${
501+
className={`text-xs px-2 py-1 rounded-md transition-colors ${PHONE_TAP} max-md:min-w-11 max-md:px-3 ${
483502
notifFilter === 'unread'
484503
? 'bg-accent text-foreground font-medium'
485504
: 'text-muted-foreground hover:text-foreground'
@@ -493,7 +512,7 @@ export function InboxPopover({
493512
<button
494513
type="button"
495514
onClick={() => setNotifFilter('all')}
496-
className={`text-xs px-2 py-1 rounded-md transition-colors ${
515+
className={`text-xs px-2 py-1 rounded-md transition-colors ${PHONE_TAP} max-md:min-w-11 max-md:px-3 ${
497516
notifFilter === 'all'
498517
? 'bg-accent text-foreground font-medium'
499518
: 'text-muted-foreground hover:text-foreground'
@@ -624,7 +643,7 @@ export function InboxPopover({
624643
<button
625644
type="button"
626645
onClick={goToAllNotifications}
627-
className="text-xs text-primary hover:underline"
646+
className={`text-xs text-primary hover:underline ${PHONE_TAP} max-md:w-full`}
628647
>
629648
{t('notifications.viewAll', { defaultValue: 'View all notifications' })}
630649
</button>
@@ -642,7 +661,7 @@ export function InboxPopover({
642661
count: pendingApprovalsCount,
643662
})}
644663
</div>
645-
<Button size="sm" className="mt-3" onClick={goToApprovals}>
664+
<Button size="sm" className={`mt-3 ${PHONE_TAP}`} onClick={goToApprovals}>
646665
{t('notifications.viewApprovals', { defaultValue: 'View approvals' })}
647666
</Button>
648667
</>
@@ -656,7 +675,7 @@ export function InboxPopover({
656675
<button
657676
type="button"
658677
onClick={goToApprovals}
659-
className="mt-2 text-xs text-primary hover:underline"
678+
className={`mt-2 text-xs text-primary hover:underline ${PHONE_TAP} max-md:px-3`}
660679
>
661680
{t('notifications.openApprovalsInbox', { defaultValue: 'Open Approvals Inbox' })}
662681
</button>
@@ -731,7 +750,7 @@ export function InboxPopover({
731750
<button
732751
type="button"
733752
onClick={goToAllActivity}
734-
className="text-xs text-primary hover:underline"
753+
className={`text-xs text-primary hover:underline ${PHONE_TAP} max-md:w-full`}
735754
>
736755
{t('layout.activityFeed.viewAll', { defaultValue: 'View all activity' })}
737756
</button>

‎ui/packages/app-shell/src/layout/NotificationPreferencesMenu.tsx‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ import { Switch } from '@object-ui/components';
3939
import { useObjectTranslation } from '@object-ui/i18n';
4040
import { useNotificationPreferences } from '../hooks/notificationPreferences.js';
4141

42+
/**
43+
* Each row is the `label` of its switch, so the whole row toggles it: the
44+
* switch alone is 44 × 24, under the 44 px tap floor, and below `md` (the
45+
* phone app's UI) the row is 44 px tall.
46+
*/
47+
const ROW_CLASS = 'flex cursor-pointer items-center justify-between gap-3 px-2 py-1.5 text-sm max-md:min-h-11';
48+
4249
export function NotificationPreferencesMenu() {
4350
const { t } = useObjectTranslation();
4451
const {
@@ -65,7 +72,7 @@ export function NotificationPreferencesMenu() {
6572

6673
return (
6774
<>
68-
<div className="flex items-center justify-between gap-3 px-2 py-1.5 text-sm">
75+
<label className={ROW_CLASS}>
6976
<span className="text-foreground/80">
7077
{t('notifications.toastEnabled', { defaultValue: 'In-app alerts' })}
7178
</span>
@@ -75,8 +82,8 @@ export function NotificationPreferencesMenu() {
7582
data-testid="notification-toast-toggle"
7683
aria-label={t('notifications.toastEnabled', { defaultValue: 'In-app alerts' })}
7784
/>
78-
</div>
79-
<div className="flex items-center justify-between gap-3 px-2 py-1.5 text-sm">
85+
</label>
86+
<label className={ROW_CLASS}>
8087
<span className={desktopDisabled ? 'text-muted-foreground' : 'text-foreground/80'}>
8188
{t('notifications.desktopEnabled', { defaultValue: 'Desktop notifications' })}
8289
</span>
@@ -91,7 +98,7 @@ export function NotificationPreferencesMenu() {
9198
data-testid="notification-desktop-toggle"
9299
aria-label={t('notifications.desktopEnabled', { defaultValue: 'Desktop notifications' })}
93100
/>
94-
</div>
101+
</label>
95102
{hint && (
96103
<p className="px-2 pb-1.5 text-xs text-muted-foreground" data-testid="notification-desktop-hint">
97104
{hint}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* The inbox fits a phone and its controls are tappable there (objectstack#29076).
5+
*
6+
* The mobile app hosts the Console, so below `md` this popover is a phone UI.
7+
* Measured on a 375 px viewport, the popover was `w-96` (384 px) and lost its
8+
* right 9 px, and its own controls were 16–36 px tall. These pin the two
9+
* rules: the popover is capped at the viewport less a gutter, and every
10+
* control the popover draws besides the rows (which are tall by content)
11+
* carries the phone tap floor.
12+
*
13+
* jsdom has no layout, so these read the classes that carry the rule; the
14+
* pixel reading is the run record on the card.
15+
*
16+
* Nothing is mocked: the real Radix popover and tabs, router and i18n pack.
17+
*/
18+
19+
import React from 'react';
20+
import { afterEach, describe, expect, it } from 'vitest';
21+
import { act, cleanup, fireEvent, render, screen, within } from '@testing-library/react';
22+
import { MemoryRouter } from 'react-router-dom';
23+
import { I18nProvider } from '@object-ui/i18n';
24+
25+
import { InboxPopover, type InboxNotification } from '../InboxPopover';
26+
import type { ActivityItem } from '../ActivityFeed';
27+
28+
afterEach(() => {
29+
cleanup();
30+
window.sessionStorage.clear();
31+
});
32+
33+
const PHONE_TAP = 'max-md:min-h-11';
34+
35+
const notifications: InboxNotification[] = [
36+
{ id: 'n1', type: 'task.assigned', title: 'Task assigned', is_read: false, created_at: '2026-10-01T10:00:00Z' },
37+
{ id: 'n2', type: 'task.assigned', title: 'Task assigned', is_read: false, created_at: '2026-10-01T10:01:00Z' },
38+
{ id: 'n3', type: 'lead.assigned', title: 'Lead assigned', is_read: false, created_at: '2026-10-01T10:02:00Z' },
39+
];
40+
41+
const activities: ActivityItem[] = [
42+
{ id: 'a1', type: 'update', objectName: 'account', user: 'Priya', description: 'updated a record', timestamp: '2026-10-01T10:00:00Z' },
43+
];
44+
45+
function bell(pendingApprovalsCount: number) {
46+
return (
47+
<MemoryRouter>
48+
<I18nProvider config={{ defaultLanguage: 'en', detectBrowserLanguage: false }} persistLanguage={false}>
49+
<InboxPopover
50+
notifications={notifications}
51+
unreadCount={notifications.length}
52+
pendingApprovalsCount={pendingApprovalsCount}
53+
activity={{ value: activities, status: 'ready', readable: true }}
54+
onMarkAllRead={() => {}}
55+
onMarkRead={() => {}}
56+
/>
57+
</I18nProvider>
58+
</MemoryRouter>
59+
);
60+
}
61+
62+
function openInbox() {
63+
fireEvent.click(screen.getByRole('button', { name: 'Open inbox' }));
64+
return screen.getByRole('dialog');
65+
}
66+
67+
function choose(dialog: HTMLElement, name: RegExp) {
68+
act(() => {
69+
fireEvent.mouseDown(within(dialog).getByRole('tab', { name }), { button: 0, ctrlKey: false });
70+
});
71+
}
72+
73+
describe('InboxPopover at phone width (objectstack#29076)', () => {
74+
it('is never wider than the viewport less a 0.5 rem gutter on each side', () => {
75+
render(bell(0));
76+
const dialog = openInbox();
77+
expect(dialog.className).toContain('max-w-[calc(100vw-1rem)]');
78+
// the desktop width stays
79+
expect(dialog.className).toContain('w-96');
80+
});
81+
82+
it('gives the Notifications tab controls the 44 px phone floor', () => {
83+
render(bell(0));
84+
const dialog = openInbox();
85+
for (const name of [/^Mark all read$/, /^Unread/, /^All$/, /^View all notifications$/]) {
86+
expect(within(dialog).getByRole('button', { name }).className, String(name)).toContain(PHONE_TAP);
87+
}
88+
// "All" is one short word: it is 44 px wide only because of its own floor
89+
expect(within(dialog).getByRole('button', { name: /^All$/ }).className).toContain('max-md:min-w-11');
90+
});
91+
92+
it('gives all three tabs the floor and drops their icons below md so they share the strip', () => {
93+
render(bell(2));
94+
const dialog = openInbox();
95+
const tabs = within(dialog).getAllByRole('tab');
96+
expect(tabs).toHaveLength(3);
97+
for (const tab of tabs) {
98+
expect(tab.className).toContain(PHONE_TAP);
99+
const icon = tab.querySelector('svg');
100+
expect(icon?.getAttribute('class')).toContain('max-md:hidden');
101+
}
102+
});
103+
104+
it('gives the Approvals tab button, in both of its states, the floor', () => {
105+
render(bell(2));
106+
let dialog = openInbox();
107+
choose(dialog, /Approvals/);
108+
expect(within(dialog).getByRole('button', { name: 'View approvals' }).className).toContain(PHONE_TAP);
109+
cleanup();
110+
111+
render(bell(0));
112+
dialog = openInbox();
113+
choose(dialog, /Approvals/);
114+
expect(within(dialog).getByRole('button', { name: 'Open Approvals Inbox' }).className).toContain(PHONE_TAP);
115+
});
116+
117+
it('gives the Activity footer the floor', () => {
118+
render(bell(0));
119+
const dialog = openInbox();
120+
choose(dialog, /Activity/);
121+
expect(within(dialog).getByRole('button', { name: 'View all activity' }).className).toContain(PHONE_TAP);
122+
});
123+
});

0 commit comments

Comments
 (0)