fix: resolve final workbench re-review findings

This commit is contained in:
2026-07-19 20:36:29 +08:00
parent e289b1dbb0
commit 2d82705679
10 changed files with 246 additions and 20 deletions

View File

@@ -4,17 +4,31 @@
export let item: InspectorItem | null;
let activeTab: 'discussion' | 'properties' | 'more' = 'discussion';
let copyFeedback = '';
let copyFailed = false;
let currentItem: InspectorItem | null = null;
$: if (item !== currentItem) {
currentItem = item;
copyFeedback = '';
copyFailed = false;
}
function copyLink() {
async function copyLink() {
const link = new URL(`?item=${encodeURIComponent(item?.title ?? '')}`, window.location.href).toString();
void navigator.clipboard?.writeText(link);
copyFeedback = 'Link copied to clipboard.';
if (!navigator.clipboard?.writeText) {
copyFailed = true;
copyFeedback = 'Clipboard access is unavailable.';
return;
}
try {
await navigator.clipboard.writeText(link);
copyFailed = false;
copyFeedback = 'Link copied to clipboard.';
} catch {
copyFailed = true;
copyFeedback = 'Unable to copy link.';
}
}
</script>
@@ -42,7 +56,7 @@
{:else}
<button type="button" on:click={copyLink}>Copy link</button>
{#if copyFeedback}
<p class="copy-feedback" role="status">{copyFeedback}</p>
<p class:copy-error={copyFailed} class="copy-feedback" role="status">{copyFeedback}</p>
{/if}
<button type="button" aria-label="Archive is unavailable in this preview" title="Archive is unavailable in this preview" disabled>Archive</button>
{/if}

View File

@@ -0,0 +1,73 @@
import '@testing-library/jest-dom/vitest';
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/svelte';
import { afterEach, describe, expect, it, vi } from 'vitest';
import ObjectInspector from './ObjectInspector.svelte';
const item = {
title: 'Review navigation',
type: 'Task',
description: 'Check the project navigation flow.',
properties: [],
};
const originalClipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard');
function setClipboard(writeText: Clipboard['writeText']) {
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText } as Clipboard,
});
}
afterEach(() => {
cleanup();
if (originalClipboard) {
Object.defineProperty(navigator, 'clipboard', originalClipboard);
} else {
Reflect.deleteProperty(navigator, 'clipboard');
}
});
describe('ObjectInspector', () => {
async function openMoreActions() {
await fireEvent.click(screen.getByRole('button', { name: 'More' }));
}
it('shows copy success only after clipboard writing resolves', async () => {
let resolveWrite: (() => void) | undefined;
const writeText = vi.fn(
() =>
new Promise<void>((resolve) => {
resolveWrite = resolve;
}),
);
setClipboard(writeText);
render(ObjectInspector, { props: { item } });
await openMoreActions();
await fireEvent.click(screen.getByRole('button', { name: 'Copy link' }));
expect(screen.queryByRole('status')).not.toBeInTheDocument();
resolveWrite?.();
await waitFor(() => expect(screen.getByRole('status')).toHaveTextContent('Link copied to clipboard.'));
});
it('reports a copy failure when clipboard writing rejects', async () => {
setClipboard(vi.fn().mockRejectedValue(new Error('Permission denied')));
render(ObjectInspector, { props: { item } });
await openMoreActions();
await fireEvent.click(screen.getByRole('button', { name: 'Copy link' }));
await waitFor(() => expect(screen.getByRole('status')).toHaveTextContent('Unable to copy link.'));
});
it('reports unavailable clipboard access without showing success', async () => {
Reflect.deleteProperty(navigator, 'clipboard');
render(ObjectInspector, { props: { item } });
await openMoreActions();
await fireEvent.click(screen.getByRole('button', { name: 'Copy link' }));
expect(screen.getByRole('status')).toHaveTextContent('Clipboard access is unavailable.');
});
});

View File

@@ -1,6 +1,6 @@
import '@testing-library/jest-dom/vitest';
import { fireEvent, render, screen, within } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';
import { fireEvent, render, screen, waitFor, within } from '@testing-library/svelte';
import { describe, expect, it, vi } from 'vitest';
import ProjectWorkbench from './ProjectWorkbench.svelte';
describe('ProjectWorkbench', () => {
@@ -88,6 +88,11 @@ describe('ProjectWorkbench', () => {
});
it('provides Copy link feedback and disables unavailable archive actions', async () => {
const originalClipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard');
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText: vi.fn().mockResolvedValue(undefined) } as Clipboard,
});
render(ProjectWorkbench, { props: { currentUser: { account: 'david@example.com' } } });
const inspector = screen.getByLabelText('Object inspector');
@@ -96,7 +101,13 @@ describe('ProjectWorkbench', () => {
await fireEvent.click(within(inspector).getByRole('button', { name: 'More' }));
await fireEvent.click(within(inspector).getByRole('button', { name: 'Copy link' }));
expect(within(inspector).getByRole('status')).toHaveTextContent('Link copied to clipboard.');
await waitFor(() => expect(within(inspector).getByRole('status')).toHaveTextContent('Link copied to clipboard.'));
expect(within(inspector).getByRole('button', { name: 'Archive is unavailable in this preview' })).toBeDisabled();
if (originalClipboard) {
Object.defineProperty(navigator, 'clipboard', originalClipboard);
} else {
Reflect.deleteProperty(navigator, 'clipboard');
}
});
});

View File

@@ -3,13 +3,22 @@
export let channel: WorkbenchChannel;
let copyStatus = '';
let copyFailed = false;
async function copyUrl() {
if (!navigator.clipboard?.writeText) {
copyFailed = true;
copyStatus = 'Copy unavailable in this browser.';
return;
}
try {
await navigator.clipboard?.writeText(channel.url ?? '');
await navigator.clipboard.writeText(channel.url ?? '');
copyFailed = false;
copyStatus = 'URL copied.';
} catch {
copyStatus = 'Copy unavailable in this browser.';
copyFailed = true;
copyStatus = 'Unable to copy URL.';
}
}
</script>
@@ -20,6 +29,6 @@
<a class="custom-link" href={channel.url} target="_blank" rel="noreferrer">Open external channel</a>
<button class="copy-url-button" type="button" on:click={copyUrl}>Copy URL</button>
{#if copyStatus}
<p class="copy-feedback" aria-live="polite">{copyStatus}</p>
<p class:copy-error={copyFailed} class="copy-feedback" aria-live="polite">{copyStatus}</p>
{/if}
</section>

View File

@@ -0,0 +1,36 @@
import '@testing-library/jest-dom/vitest';
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/svelte';
import { afterEach, describe, expect, it } from 'vitest';
import CustomLinkChannel from './CustomLinkChannel.svelte';
const channel = {
id: 'project-custom-link',
projectID: 1,
type: 'custom_link' as const,
title: 'Roadmap Board',
icon: 'link',
url: 'https://example.com/roadmap',
sortOrder: 1,
};
const originalClipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard');
afterEach(() => {
cleanup();
if (originalClipboard) {
Object.defineProperty(navigator, 'clipboard', originalClipboard);
} else {
Reflect.deleteProperty(navigator, 'clipboard');
}
});
describe('CustomLinkChannel', () => {
it('reports unavailable clipboard access without showing URL copied', async () => {
Reflect.deleteProperty(navigator, 'clipboard');
render(CustomLinkChannel, { props: { channel } });
await fireEvent.click(screen.getByRole('button', { name: 'Copy URL' }));
await waitFor(() => expect(screen.getByText('Copy unavailable in this browser.')).toBeInTheDocument());
expect(screen.queryByText('URL copied.')).not.toBeInTheDocument();
});
});