From 93eaf0002af6b5314430ce88b625ca25ad718d5c Mon Sep 17 00:00:00 2001 From: HandyWote Date: Wed, 9 Sep 2026 10:25:29 +0800 Subject: [PATCH] =?UTF-8?q?fix(shell):=20=E9=80=80=E5=87=BA=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E5=8A=A0=20flex-shrink:0/nowrap=EF=BC=8C=E9=95=BF?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=90=8D=E4=B8=8B=E4=B8=8D=E5=86=8D=E7=AB=96?= =?UTF-8?q?=E6=8E=92=EF=BC=88#75=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .shell-logout 永不收缩、文案单行,超宽压力转嫁用户名 ellipsis 截断 - apps/shell/vite.config.ts 开 test.css:true,jsdom getComputedStyle 可级联 scoped CSS - App.test.ts 新增长/短用户名布局契约用例(破坏性回验:移除任一属性即红) Signed-off-by: HandyWote --- apps/shell/src/App.test.ts | 57 ++++++++++++++++++++++++++++++++++++++ apps/shell/src/App.vue | 4 +++ apps/shell/vite.config.ts | 6 ++++ 3 files changed, 67 insertions(+) diff --git a/apps/shell/src/App.test.ts b/apps/shell/src/App.test.ts index cc43903..41871f9 100644 --- a/apps/shell/src/App.test.ts +++ b/apps/shell/src/App.test.ts @@ -164,3 +164,60 @@ describe('App.vue 模块桥挂载时机(#71 根因 2)', () => { wrapper.unmount() }) }) + +/** + * #75:侧栏 footer 的退出按钮在长用户名下被 flex-shrink 压窄, + * 「退出」按 CJK min-content 逐字换行 → 竖排。 + * 契约:按钮永不收缩(flex-shrink:0)、文案单行(white-space:nowrap), + * 挤压压力按设计转嫁给用户名的 ellipsis 截断。 + */ +describe('App.vue 侧栏退出按钮不被长用户名压窄(#75)', () => { + /** 真机复现用的长邮箱名(118 + 8 + 64 > 220 - padding 的 footer 可用宽)。 */ + const LONG_NAME = 'handy@unself.demo.example' + const SHORT_NAME = '黄一' + + /** 以指定用户名挂载(会话真值来自服务端 /api/me);空模块清单避免 iframe 干扰。 */ + async function mountAs(name: string) { + vi.mocked(fetchMe).mockResolvedValue({ + authenticated: true, + user: { id: 'u1', name, issuer: 'unself', sub: 'u1' }, + }) + vi.mocked(fetchEnabledModules).mockResolvedValue([]) + const wrapper = mount(App) + await settle() + return wrapper + } + + function computedOf(wrapper: ReturnType, selector: string) { + const el = wrapper.find(selector).element as HTMLElement + return getComputedStyle(el) + } + + /** 断言退出按钮的「不收缩 + 单行」契约,以及挤压压力的去处。 */ + function expectLogoutContract(wrapper: ReturnType) { + const box = computedOf(wrapper, '.shell-logout') + expect(box.flexShrink).toBe('0') + expect(box.whiteSpace).toBe('nowrap') + expect(box.height).toBe('30px') + expect(wrapper.find('.shell-logout').text()).toBe('退出') + + // 长名挤压时压力落在用户名截断(设计意图),而不是把按钮压窄 + const nameBox = computedOf(wrapper, '.shell-user-name') + expect(nameBox.overflow).toBe('hidden') + expect(nameBox.textOverflow).toBe('ellipsis') + } + + it('长用户名:退出按钮保持内容宽度、文案单行(不竖排)', async () => { + const wrapper = await mountAs(LONG_NAME) + expect(wrapper.find('.shell-user-name').text()).toBe(LONG_NAME) + expectLogoutContract(wrapper) + wrapper.unmount() + }) + + it('短用户名回归:同一契约不被破坏(正常场景无副作用)', async () => { + const wrapper = await mountAs(SHORT_NAME) + expect(wrapper.find('.shell-user-name').text()).toBe(SHORT_NAME) + expectLogoutContract(wrapper) + wrapper.unmount() + }) +}) diff --git a/apps/shell/src/App.vue b/apps/shell/src/App.vue index f018576..06e235e 100644 --- a/apps/shell/src/App.vue +++ b/apps/shell/src/App.vue @@ -467,6 +467,10 @@ void fetchModuleToken display: inline-flex; align-items: center; gap: var(--space-1); + /* #75:长用户名挤压 footer 时,退出按钮不得被 flex-shrink 压窄, + 否则「退出」按 CJK min-content 逐字换行变竖排。压力交给用户名省略号。 */ + flex-shrink: 0; + white-space: nowrap; height: 30px; padding: 0 var(--space-2); border: none; diff --git a/apps/shell/vite.config.ts b/apps/shell/vite.config.ts index cac11e4..33ddb38 100644 --- a/apps/shell/vite.config.ts +++ b/apps/shell/vite.config.ts @@ -1,8 +1,14 @@ // SPDX-License-Identifier: AGPL-3.0-only +/// import tailwindcss from '@tailwindcss/vite' import vue from '@vitejs/plugin-vue' import { defineConfig } from 'vite' export default defineConfig({ plugins: [vue(), tailwindcss()], + test: { + // #75:组件级布局回归需要 getComputedStyle 能级联到 SFC scoped CSS + // (jsdom 不做布局,getBoundingClientRect 恒为 0,故以计算样式契约断言)。 + css: true, + }, })