diff --git a/apps/web/src/fleet/fleet-identity-panel.test.tsx b/apps/web/src/fleet/fleet-identity-panel.test.tsx index 8bac322..8cc7802 100644 --- a/apps/web/src/fleet/fleet-identity-panel.test.tsx +++ b/apps/web/src/fleet/fleet-identity-panel.test.tsx @@ -59,6 +59,7 @@ describe('FleetIdentityPanel', () => { render( {}} onChanged={() => {}} />); const dialog = await screen.findByRole('dialog', { name: '设备身份核对' }); + await screen.findByText('已记录 2 台 · 待确认 1 台'); expect(dialog.textContent).toContain('已记录 2 台 · 待确认 1 台'); expect(screen.getByText('机房 A')).toBeTruthy(); expect(screen.getByText('同一 IMEI 出现在多台设备记录上')).toBeTruthy(); diff --git a/apps/web/src/fleet/fleet-organization-panel.test.tsx b/apps/web/src/fleet/fleet-organization-panel.test.tsx index e3a7b80..ac45d12 100644 --- a/apps/web/src/fleet/fleet-organization-panel.test.tsx +++ b/apps/web/src/fleet/fleet-organization-panel.test.tsx @@ -40,6 +40,7 @@ describe('FleetOrganizationPanel', () => { ); expect(await screen.findByRole('dialog', { name: '分组与标签' })).toBeTruthy(); + await screen.findByRole('button', { name: '编辑分组 核心' }); const core = screen.getByRole('button', { name: '编辑分组 核心' }); const row = core.closest('li'); expect(row?.textContent).toContain('2 台设备'); diff --git a/apps/web/src/instances/device-network-module.test.tsx b/apps/web/src/instances/device-network-module.test.tsx index e395457..bf9257b 100644 --- a/apps/web/src/instances/device-network-module.test.tsx +++ b/apps/web/src/instances/device-network-module.test.tsx @@ -148,7 +148,8 @@ describe('Phase 6.3 Device Network read module', () => { within(screen.getByRole('region', { name: '接口与地址' })).queryByRole('button'), ).toBeNull(); // Actions come from the pinned catalog; without a reachable API the card stays honest. - const actions = await screen.findByRole('region', { name: '设备操作' }); + await screen.findByText('无法读取该模块的可用操作,请稍后重试。'); + const actions = screen.getByRole('region', { name: '设备操作' }); expect(actions.textContent).toContain('无法读取该模块的可用操作'); }); diff --git a/apps/web/src/settings/connection-settings-panel.test.tsx b/apps/web/src/settings/connection-settings-panel.test.tsx index 2e60931..cc6bb90 100644 --- a/apps/web/src/settings/connection-settings-panel.test.tsx +++ b/apps/web/src/settings/connection-settings-panel.test.tsx @@ -1,5 +1,5 @@ // @vitest-environment jsdom -import { cleanup, render, screen, within } from '@testing-library/react'; +import { cleanup, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, describe, expect, it, vi } from 'vitest'; @@ -28,6 +28,11 @@ async function mount(overrides: Partial = {}) { const data = source(overrides); const view = render(); const region = await screen.findByRole('region', { name: '连接设置' }); + if (!overrides.connection) { + await waitFor(() => { + expect(within(region).getByLabelText('心跳间隔(秒)').value).toBe('30'); + }); + } return { ...view, data, region }; } diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 5a10ca7..790cafa 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -321,7 +321,7 @@ code { font-size: 0.76rem; } .sensitive-reveal-toggle { - min-height: 2rem; + min-height: var(--button-height-compact); padding: 0 0.7rem; color: #cdd9e8; border: 1px solid rgba(154, 175, 201, 0.22); @@ -370,7 +370,7 @@ code { background: rgba(255, 255, 255, 0.05); } .global-navigation a { - min-height: 2.25rem; + min-height: var(--button-height); display: inline-flex; align-items: center; gap: 0.4rem; @@ -513,10 +513,10 @@ main > section > header p, font-size: 0.98rem; } .fleet-sidebar-toggle { - width: 2.15rem; - min-width: 2.15rem; - height: 2.15rem; - min-height: 2.15rem; + width: var(--button-height-compact); + min-width: var(--button-height-compact); + height: var(--button-height-compact); + min-height: var(--button-height-compact); display: grid; place-items: center; padding: 0; @@ -599,10 +599,10 @@ main > section > header p, align-items: center; justify-content: space-between; gap: 0.5rem; - min-height: 2.2rem; + min-height: var(--button-height-compact); padding: 0.35rem 0.65rem; border: 1px solid var(--line); - border-radius: 9px; + border-radius: var(--button-radius-compact); color: var(--ink); background: var(--surface-soft); text-decoration: none; @@ -691,7 +691,7 @@ button.fleet-cross-health-item { gap: 0.4rem; } .fleet-status-chip { - min-height: 2.2rem; + min-height: var(--button-height-compact); display: flex; justify-content: space-between; gap: 0.4rem; @@ -726,7 +726,7 @@ button.fleet-cross-health-item { flex-wrap: wrap; } .fleet-active-chip { - min-height: 2rem; + min-height: var(--button-height-compact); max-width: 100%; display: inline-flex; gap: 0.35rem; @@ -742,7 +742,7 @@ button.fleet-cross-health-item { white-space: nowrap; } .fleet-clear-filters { - min-height: 2rem; + min-height: var(--button-height-compact); padding: 0.25rem 0.5rem; color: var(--muted); font-size: 0.72rem; @@ -803,7 +803,7 @@ button.fleet-cross-health-item { flex-wrap: wrap; } .batch-entry-actions button { - min-height: 2.15rem; + min-height: var(--button-height-compact); font-size: 0.74rem; } .batch-progress { @@ -845,7 +845,7 @@ button.fleet-cross-health-item { } .fleet-toolbar-controls select, .fleet-select-all button { - min-height: 2.25rem; + min-height: var(--button-height-compact); font-size: 0.77rem; } .fleet-card-section { @@ -1890,13 +1890,22 @@ button.fleet-cross-health-item { font-size: 0.74rem; } .instance-header-actions a { + min-height: var(--button-height-compact); + display: inline-flex; + align-items: center; padding: 0.3rem 0.55rem; + color: var(--button-secondary-text); color: #d8e5f2; - border: 1px solid rgba(153, 177, 202, 0.2); - border-radius: 7px; - background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--button-secondary-border); + border-radius: var(--button-radius-compact); + background: var(--button-secondary-bg); text-decoration: none; } +.instance-header-actions a:hover { + color: var(--button-secondary-hover-text); + border-color: var(--button-secondary-hover-border); + background: var(--button-secondary-hover-bg); +} .instance-context nav { margin: 0.8rem -1.1rem 0; overflow-x: auto; @@ -3426,7 +3435,7 @@ a.icon-button:hover { margin-top: 0.1rem; } .risk-confirm:has(input:disabled) { - opacity: 0.62; + opacity: 1; } .schedule-drawer .schedule-confirmation { background: #f3faf6; @@ -3899,8 +3908,9 @@ a.icon-button:hover { display: inline-flex; align-items: center; justify-content: center; - width: 2rem; - height: 2rem; + width: var(--icon-button-size); + height: var(--icon-button-size); + min-height: var(--icon-button-size); border: 1px solid transparent; border-radius: 50%; color: var(--workbench-muted); @@ -4871,7 +4881,7 @@ main > section > header h1, } .fleet-card-entry:active { - opacity: 0.68; + transform: translateY(1px); } .fleet-card h2 { @@ -5696,7 +5706,7 @@ svg { .fleet-group-tabs button, .fleet-group-tabs button:hover:not(:disabled) { - min-height: 1.9rem; + min-height: var(--button-height-compact); flex: 0 0 auto; padding: 0 0.72rem; border: 1px solid transparent; @@ -5772,7 +5782,7 @@ svg { .fleet-subnavigation a { display: inline-flex; align-items: center; - min-height: 1.9rem; + min-height: var(--button-height-compact); flex: 0 0 auto; padding: 0 0.72rem; border: 1px solid transparent; @@ -6549,7 +6559,7 @@ button.fleet-card-menu-trigger:focus-visible { .automation-workspace .row-actions button, .automation-workspace .row-actions button:hover:not(:disabled) { - min-height: 2.25rem; + min-height: var(--button-height-compact); padding: 0 0.45rem; border: 0; border-radius: 8px; @@ -6815,7 +6825,7 @@ button.fleet-card-menu-trigger:focus-visible { border-color: var(--workbench-danger-border); background: var(--workbench-danger-soft); cursor: default; - opacity: 0.75; + opacity: 1; } .fleet-messages-device-item[data-unavailable] small { color: var(--workbench-danger); @@ -7714,7 +7724,7 @@ button.fleet-card-menu-trigger:focus-visible { margin-top: 0.6rem; } .fleet-log-retention-actions .primary-action { - min-height: 2rem; + min-height: var(--button-height-compact); display: inline-flex; align-items: center; gap: 0.35rem; @@ -8193,7 +8203,7 @@ button.fleet-card-menu-trigger:focus-visible { [class*='animal-btn-danger'][class*='animal-btn-default'], [class*='animal-btn-danger'][class*='animal-btn-dashed'] { color: var(--button-danger-text); - border-color: rgba(182, 77, 72, 0.45); + border-color: var(--button-danger-bg); background: var(--workbench-danger-soft); } diff --git a/apps/web/src/ui/button-contrast.audit.test.tsx b/apps/web/src/ui/button-contrast.audit.test.tsx index 3a50899..c8599c9 100644 --- a/apps/web/src/ui/button-contrast.audit.test.tsx +++ b/apps/web/src/ui/button-contrast.audit.test.tsx @@ -66,7 +66,7 @@ afterEach(() => { }); describe('painted control contrast', () => { - it('keeps every rendered control at WCAG AA', { timeout: 30_000 }, async () => { + it('keeps every rendered control at WCAG AA', { timeout: 60_000 }, async () => { const raw = await readFile( new URL('file:///Users/chick/.local/opt/multi-simadmin/apps/web/src/styles.css'), 'utf8', diff --git a/apps/web/src/ui/button-system.test.ts b/apps/web/src/ui/button-system.test.ts index 7f6c6a4..98292a8 100644 --- a/apps/web/src/ui/button-system.test.ts +++ b/apps/web/src/ui/button-system.test.ts @@ -196,10 +196,62 @@ describe('button system', () => { '', ); expect(css).not.toMatch( - /#8a6722|#fff8e8|#fffafa|rgba\(17,\s*119,\s*110|rgba\(170,\s*120,\s*35|rgba\(183,\s*77,\s*72/, + /#8a6722|#fff8e8|#fffafa|rgba\(17,\s*119,\s*110|rgba\(170,\s*120,\s*35|rgba\(182,\s*77,\s*72/, ); }); + it('keeps disabled and unavailable states legible without fading controls', async () => { + const { rules } = await loadStylesheet(); + const stateSelectors = [ + '.risk-confirm:has(input:disabled)', + '.fleet-messages-device-item[data-unavailable]', + '.fleet-log-retention-value:disabled', + '.fleet-notification-entry-button:disabled', + '[class*=animal-btn-]:disabled', + ]; + const faded = rules + .filter( + (rule) => + stateSelectors.some((selector) => rule.selector.includes(selector)) && + rule.declarations.opacity !== undefined && + rule.declarations.opacity !== '1', + ) + .map((rule) => rule.selector); + + expect(faded).toEqual([]); + }); + + it('does not leave stale compact command-control sizes in later overrides', async () => { + const { rules } = await loadStylesheet(); + const compactControls = [ + '.global-navigation a', + '.fleet-sidebar-toggle', + '.fleet-cross-health-item', + '.fleet-select-all button', + '.batch-entry-actions button', + '.fleet-card-context-trigger', + '.fleet-group-tabs button', + '.fleet-subnavigation a', + '.automation-workspace .row-actions button', + '.fleet-log-retention-actions .primary-action', + ]; + const stale = rules + .filter((rule) => + compactControls.some((selector) => rule.selector.replace(/\s+/g, ' ').includes(selector)), + ) + .filter((rule) => { + const heights = [rule.declarations['min-height'], rule.declarations.height].filter( + (value): value is string => value !== undefined, + ); + return heights.some((value) => + ['1.9rem', '2rem', '2.15rem', '2.2rem', '2.25rem'].includes(value), + ); + }) + .map((rule) => rule.selector); + + expect(stale).toEqual([]); + }); + it('keeps workbench text and focus indicators on the accessible brand scale', async () => { const { tokens, resolve } = await loadStylesheet();