open-design/apps/web/tests/components/SettingsDialog.media.test.tsx
chaoxiaoche bcc58af931
refactor(web): rename Execution mode and tighten settings dialog UI (#1568)
* refactor(web): rename Execution mode and tighten settings dialog UI

- Rename "Settings → Execution & model" to "Settings → Execution mode"
  across the web UI, i18n keys, docs, and e2e selectors.
- Redesign SettingsDialog: kicker + title row in the modal head, a
  flatMap-driven agent grid that renders the inline test-result row
  beside the selected card, compact unavailable cards with right-aligned
  install/docs links, and an install guide that only shows when the
  user has no working agent picked.
- Trim verbose subtitle / hint copy across chat model, CLI proxy,
  media providers, custom instructions, and memory sections.
- Add an `info` Icon variant for the redesigned settings hints.
- Update e2e selectors and docs that referenced the old menu label.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(web): polish Settings dialog — media providers, skills, MCP

Media providers
- Hide internal Stub fixture provider (settingsVisible: false)
- Split provider list into Available (integrated, editable) and Coming
  Soon (collapsed <details> drawer with name/hint/Docs link only)
- Drop right-side Integrated/Configured badges from every row; all rows
  in the main list are integrated by definition; inline grey "Saved"
  chip next to the provider name is the only status indicator now
- "Saved" badge moves inline to the right of the provider name and uses
  a neutral grey treatment (was a standalone green pill below the name)
- "Reload from daemon" button shows a 2s green "✓ Reloaded" flash on
  success instead of leaving a permanent paragraph under the header;
  errors remain sticky

Skills
- Replace three pill-row filter banks (Source, Type, Category) with a
  compact single-row toolbar: search + three inline <select> dropdowns
  side by side; active filter highlighted with a stronger border

MCP server
- Shorten section hint to one line
- Move WHAT YOUR AGENT CAN DO capabilities above the client dropdown
  (motivate before asking to act)
- Move "Build the daemon first" warning below the code block where it
  contextually explains why the command might fail, not as a top-level
  error before the user has done anything
- Downgrade "Restart your client" left-border from accent orange to
  border-strong grey — it is a next step, not a warning

External MCP
- Shorten section hint to one line

Misc CSS
- Add .sr-only utility for accessible off-screen live regions
- Add button.ghost.is-success-flash for transient success feedback
- Add .library-filter-selects / .library-filter-select for dropdown
  filter rows
- Add .media-provider-coming-soon-* for the roadmap drawer

Co-authored-by: Cursor <cursoragent@cursor.com>

* [codex] Add Cursor Agent auth diagnostics (#1538)

* Add Cursor Agent auth diagnostics

* Handle Cursor not logged in auth status

* Address Cursor auth review feedback

* Classify Cursor stdout auth failures

* test: expand Memory and Routines coverage (#1521)

* test: expand settings and packaged coverage

* test: extend memory settings coverage

* test: cover routine settings failure states

* test: cover routine operation failures

* test: fix daemon test typing on CI

* test: decouple packaged smoke from orbit bug

* test: avoid live memory LLM calls in route tests

* test: fix daemon fetch typing in CI

* fix: restore preview comment and inspect toggles

* test: align manual edit flow with current inspector UX

* test: align comment attachment flow with current preview comments UI

* fix: probe resolved Codex launch path during detection

* fix: remove duplicate board activation helper after rebase

* test: update ghost cli detection mock

* test: align FileViewer toolbar expectation

* ci: move full app tests to extended lane

* ci: run app tests by changed scope

* ci: cover shared app inputs in test scopes

* ci: avoid setup-node cache in windows packaged smoke

* test: align extended settings and manual edit flows

* refactor(web): rename Execution mode and tighten settings dialog UI

- Rename "Settings → Execution & model" to "Settings → Execution mode"
  across the web UI, i18n keys, docs, and e2e selectors.
- Redesign SettingsDialog: kicker + title row in the modal head, a
  flatMap-driven agent grid that renders the inline test-result row
  beside the selected card, compact unavailable cards with right-aligned
  install/docs links, and an install guide that only shows when the
  user has no working agent picked.
- Trim verbose subtitle / hint copy across chat model, CLI proxy,
  media providers, custom instructions, and memory sections.
- Add an `info` Icon variant for the redesigned settings hints.
- Update e2e selectors and docs that referenced the old menu label.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(web): settings dialog UX polish — layout, dedup, and interactions

- Remove duplicate section headers from all settings sections
  (Notifications, Appearance, Privacy, About, Design Systems, Skills,
  MCP server, Connectors, Media providers, Routines)
- Restructure Notifications cards: title + toggle on same row, hint below
- Restructure Skills toolbar: search + New skill button in row 1,
  filter dropdowns in row 2 with left-aligned labels
- Restructure Pet section: tabs and Wake button on same row
- MCP server: group capabilities and setup into separate cards,
  remove nested double border on client picker
- Connectors: show connect errors as toast instead of inline card text,
  position toast inside panel, hide single-provider tab
- Media providers: move Reload button to left-aligned small ghost button
- Memory: info icon shows path on hover, Path copied badge inline;
  Extraction history and MEMORY.md as standalone collapsible cards;
  group header hidden when only one type visible
- Pet grid cards: Adopt button hidden until hover, icon-only when adopted,
  description truncated to 2 lines, text fills full width via abs positioning
- Agent cards: selected state uses accent border only, no background change
- Add sun/moon icons to Appearance theme buttons (Light/Dark)
- Shorten several hint strings for clarity

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): resolve i18n review comments from PR #1568

- Update settings.title and settings.envConfigure to localized
  "Execution mode" in all 17 non-English locale files
- Add settings.memoryFlashPathCopied to all locales and use t()
  in MemorySection instead of hardcoded English "Path copied"
- Add settings.agentModelHead to all locales and use t() in
  SettingsDialog for "Model for:" agent model row header

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): update tests to match settings dialog redesign

- Add role prop to Toast (alert/status) so error toasts from
  ConnectorsBrowser are announced immediately by screen readers
- Clear connectErrorToast on successful connector retry
- Update SettingsDialog.execution tests:
  - Remove heading assertions for About and MCP server (headers
    were intentionally removed as duplicate nav labels)
  - Rewrite CLI env test to use codex-only fields (per-agent
    filtering means only selected agent's fields are shown)
  - Update Composio key hint text assertion to match shortened copy
  - Replace filter button click with select change for Type filter
  - Replace Configured/Unsupported/Integrated badge checks with
    updated assertions matching the new media provider UI
  - Replace disabled BFL row test with coming-soon section check
- Update SettingsDialog.media test: remove Fal.ai input assertions
  (non-integrated providers no longer have editable fields)

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): unblock CI for #1568

Three small fixes to get Playwright back to green on the settings
dialog redesign:

1. `en.ts`: revert `settings.envConfigure` to "Configure execution mode".
   This PR collapsed both `settings.title` (header gear) and
   `settings.envConfigure` (entry-side foot pill) to the same string
   "Execution mode", so `getByRole('button', { name: 'Execution mode' })`
   resolved to two elements and tripped Playwright strict mode in the
   three Composio-flow tests (entry-configuration-flows.test.ts:174,
   228, 285). Restoring the distinct label also gives screen readers
   a clearer hint for the pill, which doubles as a status display.
   Non-English locales still alias the two keys; happy to follow up
   on those, but they don't gate the (English-only) Playwright suite.

2. entry-configuration-flows.test.ts:167 — `Connectors` heading is now
   rendered at `<h2>` in the modal-head (SettingsDialog.tsx:1545), with
   the inner `<h3>` removed by design (see comment around line 1448).
   Updated the assertion from `level: 3` to `level: 2`.

3. project-management-flows.test.ts:360 — same change for the `Pets`
   heading.

Verified locally with `pnpm --filter @open-design/web typecheck` and
`pnpm --filter @open-design/e2e typecheck`. The actual Playwright
specs need the dev server up; I didn't rerun them here, but the
locator changes are mechanical and match the new DOM.

* fix(web): use exact match for Execution mode button locator

Playwright's `getByRole({ name })` defaults to substring matching, so
`{ name: 'Execution mode' }` still resolved to both the header gear
(aria-label "Execution mode") and the entry-side foot pill (aria-label
"Configure execution mode" — substring contains "Execution mode").
Strict mode tripped in the three composio-flow tests at lines 202,
257, and 319.

Adding `exact: true` makes each call resolve to just the header gear,
which opens the same dialog the foot pill does — the test outcomes
are unchanged.

---------

Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Caprika <56862773+alchemistklk@users.noreply.github.com>
Co-authored-by: shangxinyu1 <shangxinyu@refly.ai>
Co-authored-by: lefarcen <935902669@qq.com>
2026-05-15 14:35:06 +08:00

230 lines
6.7 KiB
TypeScript

// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { within } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { SettingsDialog } from '../../src/components/SettingsDialog';
import { DEFAULT_CONFIG } from '../../src/state/config';
import type { AgentInfo, AppConfig } from '../../src/types';
describe('SettingsDialog media providers', () => {
afterEach(() => {
cleanup();
});
it('shows saved masked media provider keys like Composio does', () => {
renderDialog({
...DEFAULT_CONFIG,
mediaProviders: {
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '1234',
baseUrl: '',
},
},
});
expect(screen.getByText('Saved · ••••1234')).toBeTruthy();
expect(screen.getByLabelText('OpenAI API key').getAttribute('placeholder')).toBe(
'Paste a new key to replace the saved one',
);
});
it('shows daemon fallback notice and reloads media providers from daemon', async () => {
const reloadMock = vi.fn(async () => ({
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '9876',
baseUrl: 'https://daemon.example/v1',
},
}));
renderDialog(
{
...DEFAULT_CONFIG,
mediaProviders: {},
},
{
mediaProvidersNotice:
'Could not load media provider settings from the local daemon. Using browser-saved settings for now.',
onReloadMediaProviders: reloadMock,
},
);
expect(
screen.getByText(
'Could not load media provider settings from the local daemon. Using browser-saved settings for now.',
),
).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Reload from daemon' }));
await waitFor(() => {
expect(reloadMock).toHaveBeenCalledTimes(1);
expect(screen.getByText('Saved · ••••9876')).toBeTruthy();
expect(screen.getByText('Reloaded media provider settings from the local daemon.')).toBeTruthy();
});
expect((screen.getByLabelText('OpenAI Base URL') as HTMLInputElement).value).toBe(
'https://daemon.example/v1',
);
});
it('preserves local-only providers when daemon reload returns a partial provider set', async () => {
const reloadMock = vi.fn(async () => ({
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '9876',
baseUrl: 'https://daemon.example/v1',
},
}));
renderDialog(
{
...DEFAULT_CONFIG,
mediaProviders: {
openai: {
apiKey: 'sk-local-openai',
baseUrl: 'https://local-openai.example/v1',
},
fal: {
apiKey: 'sk-local-fal',
baseUrl: 'https://queue.fal.run',
model: 'fal-ai/imagen4/preview',
},
},
},
{
mediaProvidersNotice:
'Could not load media provider settings from the local daemon. Using browser-saved settings for now.',
onReloadMediaProviders: reloadMock,
},
);
fireEvent.click(screen.getByRole('button', { name: 'Reload from daemon' }));
await waitFor(() => {
expect(reloadMock).toHaveBeenCalledTimes(1);
expect(screen.getByText('Saved · ••••9876')).toBeTruthy();
expect(screen.getByText('Reloaded media provider settings from the local daemon.')).toBeTruthy();
});
expect((screen.getByLabelText('OpenAI Base URL') as HTMLInputElement).value).toBe(
'https://daemon.example/v1',
);
// Fal.ai is a non-integrated (coming-soon) provider and no longer has
// editable input fields in the UI; its config is preserved in state via
// mergeDaemonMediaProviders (covered by state/config.test.ts).
});
it('preserves saved media keys when clearing only a non-secret field', async () => {
const onPersist = vi.fn();
renderDialog(
{
...saveableConfig(),
mediaProviders: {
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '1234',
baseUrl: 'https://custom.example/v1',
},
},
},
{ onPersist },
);
fireEvent.change(screen.getByLabelText('OpenAI Base URL'), { target: { value: '' } });
await waitFor(() => {
expect(onPersist).toHaveBeenCalledWith(
expect.objectContaining({
mediaProviders: {
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '1234',
baseUrl: '',
},
},
}),
expect.objectContaining({ forceMediaProviderSync: true }),
);
});
});
it('clears saved media keys only through the explicit Clear action', async () => {
const onPersist = vi.fn();
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
renderDialog(
{
...saveableConfig(),
mediaProviders: {
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '1234',
baseUrl: 'https://custom.example/v1',
},
},
},
{ onPersist },
);
const openaiRow = screen.getByText('OpenAI').closest('.media-provider-row') as HTMLElement | null;
if (!openaiRow) throw new Error('Expected OpenAI media provider row');
fireEvent.click(within(openaiRow).getByRole('button', { name: 'Clear' }));
await waitFor(() => {
expect(onPersist).toHaveBeenCalledWith(
expect.objectContaining({ mediaProviders: {} }),
expect.objectContaining({ forceMediaProviderSync: true }),
);
});
expect(confirmSpy).toHaveBeenCalledTimes(1);
confirmSpy.mockRestore();
});
});
function renderDialog(
initial: AppConfig,
options?: {
mediaProvidersNotice?: string | null;
onReloadMediaProviders?: () => Promise<AppConfig['mediaProviders'] | null>;
onPersist?: (cfg: AppConfig, options?: { forceMediaProviderSync?: boolean }) => void;
},
) {
return render(
<SettingsDialog
initial={initial}
agents={SAVEABLE_AGENTS}
daemonLive
appVersionInfo={null}
initialSection="media"
onPersist={options?.onPersist ?? vi.fn()}
onPersistComposioKey={vi.fn()}
onClose={vi.fn()}
onRefreshAgents={vi.fn()}
mediaProvidersNotice={options?.mediaProvidersNotice}
onReloadMediaProviders={options?.onReloadMediaProviders}
/>,
);
}
const SAVEABLE_AGENTS: AgentInfo[] = [
{
id: 'codex',
name: 'Codex',
bin: 'codex',
available: true,
},
];
function saveableConfig(): AppConfig {
return {
...DEFAULT_CONFIG,
agentId: 'codex',
};
}