fix: resolve final workbench re-review findings
This commit is contained in:
@@ -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}
|
||||
|
||||
73
apps/web/src/features/workbench/ObjectInspector.test.ts
Normal file
73
apps/web/src/features/workbench/ObjectInspector.test.ts
Normal 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.');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user