diff --git a/docs/design/evidence/task-702/after-1440-dark.png b/docs/design/evidence/task-702/after-1440-dark.png new file mode 100644 index 0000000000..eaad7765de Binary files /dev/null and b/docs/design/evidence/task-702/after-1440-dark.png differ diff --git a/docs/design/evidence/task-702/after-1440-light-modal.png b/docs/design/evidence/task-702/after-1440-light-modal.png new file mode 100644 index 0000000000..d1d0011125 Binary files /dev/null and b/docs/design/evidence/task-702/after-1440-light-modal.png differ diff --git a/docs/design/evidence/task-702/after-1440-light.png b/docs/design/evidence/task-702/after-1440-light.png new file mode 100644 index 0000000000..7593c5b16f Binary files /dev/null and b/docs/design/evidence/task-702/after-1440-light.png differ diff --git a/docs/design/evidence/task-702/after-390-dark.png b/docs/design/evidence/task-702/after-390-dark.png new file mode 100644 index 0000000000..79234e2821 Binary files /dev/null and b/docs/design/evidence/task-702/after-390-dark.png differ diff --git a/docs/design/evidence/task-702/after-390-light.png b/docs/design/evidence/task-702/after-390-light.png new file mode 100644 index 0000000000..25243b6bfa Binary files /dev/null and b/docs/design/evidence/task-702/after-390-light.png differ diff --git a/docs/design/evidence/task-702/before-1440-dark.png b/docs/design/evidence/task-702/before-1440-dark.png new file mode 100644 index 0000000000..0950653f42 Binary files /dev/null and b/docs/design/evidence/task-702/before-1440-dark.png differ diff --git a/docs/design/evidence/task-702/before-1440-light.png b/docs/design/evidence/task-702/before-1440-light.png new file mode 100644 index 0000000000..f2003182b0 Binary files /dev/null and b/docs/design/evidence/task-702/before-1440-light.png differ diff --git a/docs/design/evidence/task-702/before-390-dark.png b/docs/design/evidence/task-702/before-390-dark.png new file mode 100644 index 0000000000..87b40a8cb8 Binary files /dev/null and b/docs/design/evidence/task-702/before-390-dark.png differ diff --git a/docs/design/evidence/task-702/before-390-light.png b/docs/design/evidence/task-702/before-390-light.png new file mode 100644 index 0000000000..c34c7f8bf8 Binary files /dev/null and b/docs/design/evidence/task-702/before-390-light.png differ diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 25680c0b90..9f1b151437 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -668,7 +668,8 @@ "agents_one": "{{count}} agent", "agents_other": "{{count}} agents", "boardOpen": "board {{count}} open", - "backToPods": "Back to pods" + "backToPods": "Back to pods", + "invite": "Invite" }, "team": { "view": "View pod team", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 3ffafea633..ab2e359cff 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -667,7 +667,8 @@ "agents_one": "{{count}}个智能体", "agents_other": "{{count}}个智能体", "boardOpen": "看板{{count}}项待办", - "backToPods": "返回 Pod 列表" + "backToPods": "返回 Pod 列表", + "invite": "邀请" }, "team": { "view": "查看 Pod 团队", diff --git a/frontend/src/v2/__tests__/V2ThreadHeaderInvite.test.tsx b/frontend/src/v2/__tests__/V2ThreadHeaderInvite.test.tsx new file mode 100644 index 0000000000..21f60c1961 --- /dev/null +++ b/frontend/src/v2/__tests__/V2ThreadHeaderInvite.test.tsx @@ -0,0 +1,90 @@ +// @ts-nocheck +// TASK-702: the pod header carries an Invite for members, opening the same +// invite modal the inspector does. Hidden for a non-member and on 1:1 pods. +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import V2Thread from '../components/V2Thread'; +import { AuthContext } from '../../context/AuthContext'; + +jest.mock('../../context/SocketContext', () => ({ + useSocket: () => ({ socket: null, connected: false }), +})); + +jest.mock('axios', () => { + const mock = { + get: jest.fn(() => Promise.resolve({ data: {} })), + post: jest.fn(() => Promise.resolve({ data: {} })), + patch: jest.fn(), + delete: jest.fn(), + defaults: { baseURL: '', headers: { common: {} } }, + interceptors: { + request: { use: jest.fn(), eject: jest.fn() }, + response: { use: jest.fn(), eject: jest.fn() }, + }, + }; + return { __esModule: true, default: mock, ...mock }; +}); + +beforeAll(() => { + Element.prototype.scrollIntoView = jest.fn(); +}); + +const authValue = { + currentUser: { _id: 'u1', username: 'alice' }, + user: { _id: 'u1', username: 'alice' }, + token: 't', + loading: false, + error: null, + isAuthenticated: true, + register: jest.fn(), + login: jest.fn(), + logout: jest.fn(), + updateProfile: jest.fn(), +}; + +const renderThread = ({ podType = 'chat', member = true, onOpenInvite = jest.fn() } = {}) => { + const detail = { + pod: { _id: 'pod-1', name: 'Launch Room', type: podType }, + members: member + ? [{ _id: 'u1', username: 'alice', isBot: false }, { _id: 'u2', username: 'bob', isBot: false }] + : [{ _id: 'u2', username: 'bob', isBot: false }], + messages: [], + agents: [], + sendMessage: jest.fn(), + loading: false, + error: null, + refresh: jest.fn(), + }; + render( + + + + + , + ); + return onOpenInvite; +}; + +describe('V2Thread header Invite', () => { + test('a member of a team pod sees Invite, and it opens the people tab', () => { + const onOpenInvite = renderThread(); + fireEvent.click(screen.getByTestId('pod-header-invite')); + expect(onOpenInvite).toHaveBeenCalledWith('people'); + }); + + test('a non-member does not see Invite', () => { + renderThread({ member: false }); + expect(screen.queryByTestId('pod-header-invite')).not.toBeInTheDocument(); + }); + + test.each(['agent-room', 'agent-dm'])('a %s pod does not show Invite', (podType) => { + renderThread({ podType }); + expect(screen.queryByTestId('pod-header-invite')).not.toBeInTheDocument(); + }); + + test('no invite handler, no Invite', () => { + renderThread({ onOpenInvite: null }); + expect(screen.queryByTestId('pod-header-invite')).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 0326b1dc6e..a83fc3fe73 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -5262,4 +5262,12 @@ describe('pod board column scroll (TASK-698)', () => { expect(focus).toContain('max-height: min(35vh, 260px)'); expect(focus).toContain('overflow-y: auto'); }); + + // TASK-702: on a phone the header Invite is glyph-only, or its label takes + // the pod name's width and the title truncates to a few characters. + test('the header Invite hides its label on a phone, keeping it for screen readers', () => { + const block = mediaBlockContaining(read('../v2.css'), '.v2-pod-header__invite span'); + expect(block).toMatch(/\.v2-pod-header__invite span\s*\{[^}]*clip-path:\s*inset\(50%\)/); + expect(block).toMatch(/\.v2-pod-header__invite\s*\{[^}]*width:\s*28px/); + }); }); diff --git a/frontend/src/v2/components/V2Thread.tsx b/frontend/src/v2/components/V2Thread.tsx index 22495b787c..f29bc81dec 100644 --- a/frontend/src/v2/components/V2Thread.tsx +++ b/frontend/src/v2/components/V2Thread.tsx @@ -1943,6 +1943,21 @@ const V2Thread: React.FC = ({ detail, firstRunVisible = false, in ))} + {/* TASK-702: the invite lived only in the inspector, where nobody + looked. Members see it here; onOpenInvite is already absent + on the 1:1 pod types. */} + {onOpenInvite && isPodMember && !isAgentDm && !isAgentRoom && ( + + )} {onToggleInspector && (