feat(web): copy-link button on owned public playlists (Tier C12)
test-web / test (push) Successful in 31s

Add a Link icon button to the playlist header that copies the
absolute /playlists/[id] URL to the clipboard. Visible only when
isOwner && pl.is_public. Falls back to a window.prompt() in
insecure contexts where navigator.clipboard is unavailable.

Tests: button visibility (private vs public owner) + clipboard
write target.
This commit is contained in:
2026-06-01 14:13:05 -04:00
parent 859aff8d30
commit cc81e0f183
2 changed files with 50 additions and 1 deletions
@@ -78,6 +78,32 @@ describe('Playlist detail page', () => {
expect(screen.queryByLabelText(/delete playlist/i)).not.toBeInTheDocument();
});
test('Copy-link button appears only when owner AND playlist is public', async () => {
// Owner + private → no button
mockedQuery.mockReturnValue(mockQuery({ data: ownDetail }));
const { unmount } = render(PlaylistDetailPage);
expect(screen.queryByLabelText(/copy public link/i)).not.toBeInTheDocument();
unmount();
// Owner + public → button appears
mockedQuery.mockReturnValue(mockQuery({ data: { ...ownDetail, is_public: true } }));
render(PlaylistDetailPage);
expect(screen.getByLabelText(/copy public link/i)).toBeInTheDocument();
});
test('Copy-link writes the absolute /playlists/[id] URL to clipboard', async () => {
mockedQuery.mockReturnValue(mockQuery({ data: { ...ownDetail, is_public: true } }));
const writeText = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true });
render(PlaylistDetailPage);
await fireEvent.click(screen.getByLabelText(/copy public link/i));
// jsdom's window.location.origin is "http://localhost" by default.
// Match by suffix to stay resilient to vitest config changes.
await waitFor(() =>
expect(writeText).toHaveBeenCalledWith(expect.stringMatching(/\/playlists\/p1$/))
);
});
test('Delete button confirms then deletes', async () => {
mockedQuery.mockReturnValue(mockQuery({ data: ownDetail }));
const confirmSpy = vi.spyOn(globalThis, 'confirm').mockReturnValue(true);