open-design/apps/web/tests/components/HandoffButton.fallback-reveal.test.tsx
leessju cfde84b038
Some checks failed
visual-baseline / Capture visual baselines (push) Waiting to run
ci / Detect CI change scopes (push) Successful in 0s
landing-page-ci / Validate landing page (push) Failing after 3s
landing-page-staging / Deploy landing page to staging (push) Has been skipped
nix-check / build (push) Failing after 2s
ci / Validate Nix flake (push) Has been skipped
ci / Preflight (push) Failing after 1s
ci / Workspace unit tests (push) Failing after 2s
ci / Daemon workspace tests (push) Failing after 1s
ci / Web workspace tests (push) Failing after 2s
ci / Browser tests (push) Failing after 1s
ci / Build workspaces (push) Failing after 1s
ci / Validate workspace (push) Failing after 0s
ci / Runtime trace (push) Has been skipped
fix(web): make hand-off no-editors fallback perform a real reveal (#2494)
* fix(web): make hand-off no-editors fallback perform a real reveal

The Finder/Explorer/File Manager fallback button was only calling an
optional onRequestRevealInFinder prop that the actual caller never
passes, so the surface advertised an action it never performed.

finder, explorer, and file-manager are real entries in the daemon's
open-in catalogue (open / explorer / xdg-open), so route the fallback
through openProjectInEditor(projectId, fallbackId) for a genuine
reveal. Keep the renderer reveal bridge as a secondary fallback if
the daemon spawn fails, and disable the button while busy so a double
click can't queue two reveals.

Adjacent: PreviewDrawOverlay's Send-while-streaming behavior is
intentional (sending is queued downstream, not blocked), and the
button already carries sendDisabledReason as its tooltip. Cover that
contract with a regression test so a future change can't silently
re-disable the control or drop the localized reason.

Scope note: the i18n hand-off key migration that previously rode on
this branch landed on main via a different key set, so this PR is
narrowed to just the fallback wire-up and the two regression tests.

* fix(web): surface daemon spawn failure inline in zero-editors fallback

The zero-editors HandoffButton fallback called setError() on a rejected
openProjectInEditor but returned only the <button>, so the error never
rendered. Production callers (ProjectView) mount the component without
onRequestRevealInFinder, so a daemon spawn failure became a silent no-op
— exactly the failure mode the PR was meant to cover.

Wrap the solo button in a handoff-wrap container and render the error
inline next to it. Adds a regression test for the rejected-spawn path.

* fix(web): align preview draw send-disabled test

* fix(web): show handoff fallback for zero editors

---------

Co-authored-by: nicejames <nicejames@gmail.com>
Co-authored-by: mrcfps <mrc@powerformer.com>
2026-05-30 06:34:12 +00:00

72 lines
2.6 KiB
TypeScript

// @vitest-environment jsdom
// Regression for the zero-editors fallback: when no editor is detected, the
// fallback button must perform a real reveal (open the project folder via the
// daemon's open-in catalogue: finder / explorer / file-manager) rather than a
// no-op that advertises an action it never runs.
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { HandoffButton } from '../../src/components/HandoffButton';
import { I18nProvider } from '../../src/i18n';
import type { HostEditorsResponse } from '@open-design/contracts';
const fetchHostEditors = vi.fn<() => Promise<HostEditorsResponse>>();
const openProjectInEditor = vi.fn();
vi.mock('../../src/providers/registry', () => ({
fetchHostEditors: () => fetchHostEditors(),
openProjectInEditor: (...args: unknown[]) => openProjectInEditor(...args),
}));
afterEach(() => {
cleanup();
fetchHostEditors.mockReset();
openProjectInEditor.mockReset();
});
describe('HandoffButton zero-editors fallback', () => {
it('opens the project folder in the OS file manager via the daemon', async () => {
fetchHostEditors.mockResolvedValue({
platform: 'darwin',
editors: [],
});
openProjectInEditor.mockResolvedValue(undefined);
render(
<I18nProvider initial="en">
<HandoffButton projectId="p1" />
</I18nProvider>,
);
const fallback = (await screen.findByText('Finder')).closest('button') as HTMLButtonElement;
fireEvent.click(fallback);
await waitFor(() => expect(openProjectInEditor).toHaveBeenCalledWith('p1', 'finder'));
});
it('surfaces a daemon spawn failure inline so the fallback is not a silent no-op', async () => {
// The production caller (`ProjectView`) mounts `<HandoffButton projectId={…} />`
// with no `onRequestRevealInFinder` callback, so a rejected
// `openProjectInEditor` would otherwise leave users with a CTA that
// advertises Finder/Explorer/File Manager but does nothing visible.
fetchHostEditors.mockResolvedValue({
platform: 'darwin',
editors: [],
});
openProjectInEditor.mockRejectedValue(new Error('daemon refused: ENOENT'));
render(
<I18nProvider initial="en">
<HandoffButton projectId="p1" />
</I18nProvider>,
);
const fallback = (await screen.findByText('Finder')).closest('button') as HTMLButtonElement;
fireEvent.click(fallback);
const errorEl = await screen.findByTestId('handoff-fallback-error');
expect(errorEl.textContent).toContain('daemon refused: ENOENT');
});
});