mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-08-31 03:07:23 +00:00
test(extensions): add tests for Extensions component
This commit is contained in:
parent
9aa77c0b01
commit
a5cc1db32f
3 changed files with 294 additions and 2 deletions
288
src/lib/components/Extensions.svelte.test.ts
Normal file
288
src/lib/components/Extensions.svelte.test.ts
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
import { mockedCore, mockedClipboard } from '$lib/__mocks__/tauri.mock';
|
||||||
|
import { render, screen, cleanup, waitFor } from '@testing-library/svelte';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import Extensions from './Extensions.svelte';
|
||||||
|
import type { Extension } from '$lib/store';
|
||||||
|
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.HTMLElement.prototype.animate = () => ({
|
||||||
|
finished: Promise.resolve(),
|
||||||
|
cancel: () => {},
|
||||||
|
play: () => {},
|
||||||
|
pause: () => {},
|
||||||
|
reverse: () => {},
|
||||||
|
finish: () => {},
|
||||||
|
commitStyles: () => {},
|
||||||
|
updatePlaybackRate: () => {},
|
||||||
|
persist: () => {},
|
||||||
|
startTime: 0,
|
||||||
|
currentTime: 0,
|
||||||
|
timeline: null,
|
||||||
|
playbackRate: 1,
|
||||||
|
pending: false,
|
||||||
|
playState: 'idle',
|
||||||
|
ready: Promise.resolve(),
|
||||||
|
onfinish: null,
|
||||||
|
oncancel: null,
|
||||||
|
onremove: null,
|
||||||
|
effect: null,
|
||||||
|
id: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock('@tauri-apps/plugin-opener', () => ({
|
||||||
|
openUrl: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockedFetch = vi.fn();
|
||||||
|
vi.mock('@tauri-apps/plugin-http', () => ({
|
||||||
|
fetch: (url: string) => mockedFetch(url)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const extensionsStore = vi.hoisted(() => ({
|
||||||
|
searchText: '',
|
||||||
|
searchResults: [] as Extension[],
|
||||||
|
selectedCategory: 'All Categories',
|
||||||
|
extensions: [] as Extension[],
|
||||||
|
featuredExtensions: [] as Extension[],
|
||||||
|
trendingExtensions: [] as Extension[],
|
||||||
|
isSearching: false,
|
||||||
|
isLoading: false,
|
||||||
|
selectedIndex: 0,
|
||||||
|
loadMore: vi.fn(),
|
||||||
|
_reset: function () {
|
||||||
|
this.searchText = '';
|
||||||
|
this.searchResults = [];
|
||||||
|
this.selectedCategory = 'All Categories';
|
||||||
|
this.extensions = [];
|
||||||
|
this.featuredExtensions = [];
|
||||||
|
this.trendingExtensions = [];
|
||||||
|
this.isSearching = false;
|
||||||
|
this.isLoading = false;
|
||||||
|
this.selectedIndex = 0;
|
||||||
|
this.loadMore.mockClear();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
vi.mock('./extensions/store.svelte', () => ({
|
||||||
|
extensionsStore
|
||||||
|
}));
|
||||||
|
|
||||||
|
const viewManager = vi.hoisted(() => ({
|
||||||
|
extensionToSelect: null,
|
||||||
|
showSettings: vi.fn(),
|
||||||
|
_reset: function () {
|
||||||
|
this.extensionToSelect = null;
|
||||||
|
this.showSettings.mockClear();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
vi.mock('$lib/viewManager.svelte', () => ({
|
||||||
|
viewManager
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockAuthor = {
|
||||||
|
name: 'Raycast',
|
||||||
|
handle: 'raycast',
|
||||||
|
avatar: 'https://raycast.com/avatar.png',
|
||||||
|
initials: 'RC',
|
||||||
|
avatar_placeholder_color: '#A067DC' as const
|
||||||
|
};
|
||||||
|
|
||||||
|
const createMockExtension = (
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
category: string,
|
||||||
|
options: Partial<Extension> = {}
|
||||||
|
): Extension => ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
native_id: null,
|
||||||
|
title: name,
|
||||||
|
description: `Description for ${name}`,
|
||||||
|
author: mockAuthor,
|
||||||
|
owner: mockAuthor,
|
||||||
|
icons: { light: 'icon.png', dark: 'icon.png' },
|
||||||
|
categories: [category],
|
||||||
|
store_url: `https://raycast.com/raycast/${name}`,
|
||||||
|
download_url: `https://raycast.com/api/v1/extensions/raycast/${name}/download`,
|
||||||
|
readme_url: `https://github.com/raycast/extensions/blob/main/extensions/${name}/README.md`,
|
||||||
|
source_url: `https://github.com/raycast/extensions/tree/main/extensions/${name}`,
|
||||||
|
seo_categories: [],
|
||||||
|
platforms: null,
|
||||||
|
created_at: Date.now(),
|
||||||
|
kill_listed_at: null,
|
||||||
|
status: 'active',
|
||||||
|
is_new: false,
|
||||||
|
access: 'public',
|
||||||
|
download_count: 100,
|
||||||
|
commit_sha: '12345',
|
||||||
|
relative_path: `extensions/${name}`,
|
||||||
|
api_version: '1.0',
|
||||||
|
prompt_examples: [],
|
||||||
|
metadata_count: 0,
|
||||||
|
updated_at: Date.now(),
|
||||||
|
readme_assets_path: '',
|
||||||
|
commands: [],
|
||||||
|
tools: [],
|
||||||
|
contributors: [],
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockFeatured = createMockExtension('1', 'Featured Extension', 'Featured');
|
||||||
|
const mockTrending = createMockExtension('2', 'Trending Extension', 'Trending');
|
||||||
|
const mockRegular = createMockExtension('3', 'Regular Extension', 'Productivity');
|
||||||
|
const mockDetailed = createMockExtension('1', 'Featured Extension', 'Featured');
|
||||||
|
|
||||||
|
describe('Extensions.svelte', () => {
|
||||||
|
const onBack = vi.fn();
|
||||||
|
const onInstall = vi.fn();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cleanup();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
extensionsStore._reset();
|
||||||
|
viewManager._reset();
|
||||||
|
mockedFetch.mockClear();
|
||||||
|
mockedCore.invoke.mockResolvedValue({ status: 'success' });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('1. Initial Rendering and State', () => {
|
||||||
|
it('should show a loading indicator while loading', () => {
|
||||||
|
extensionsStore.isLoading = true;
|
||||||
|
render(Extensions, { onBack, onInstall });
|
||||||
|
expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the header and search input', () => {
|
||||||
|
render(Extensions, { onBack, onInstall });
|
||||||
|
expect(screen.getByPlaceholderText('Search Store for extensions...')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display extensions with headers when loaded', async () => {
|
||||||
|
extensionsStore.featuredExtensions = [mockFeatured];
|
||||||
|
extensionsStore.trendingExtensions = [mockTrending];
|
||||||
|
extensionsStore.extensions = [mockRegular];
|
||||||
|
render(Extensions, { onBack, onInstall });
|
||||||
|
|
||||||
|
expect(await screen.findByText('Featured')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByText(mockFeatured.title)).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(await screen.findByText('Trending')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByText(mockTrending.title)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('2. Search and Filtering', () => {
|
||||||
|
it('should update searchText in the store when typing in search input', async () => {
|
||||||
|
render(Extensions, { onBack, onInstall });
|
||||||
|
const searchInput = screen.getByPlaceholderText('Search Store for extensions...');
|
||||||
|
await user.type(searchInput, 'test search');
|
||||||
|
expect(extensionsStore.searchText).toBe('test search');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display only search results when searchText is present', async () => {
|
||||||
|
extensionsStore.searchText = 'search';
|
||||||
|
extensionsStore.searchResults = [mockRegular];
|
||||||
|
render(Extensions, { onBack, onInstall });
|
||||||
|
|
||||||
|
expect(await screen.findByText('Search Results')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(mockRegular.title)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Featured')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display filtered extensions when a category is selected', async () => {
|
||||||
|
extensionsStore.selectedCategory = 'Productivity';
|
||||||
|
extensionsStore.extensions = [mockFeatured, mockRegular];
|
||||||
|
render(Extensions, { onBack, onInstall });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Productivity', { selector: 'button' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(screen.getByText(mockRegular.title)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(mockFeatured.title)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('3. Extension Selection and Detail View', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
extensionsStore.featuredExtensions = [mockFeatured];
|
||||||
|
mockedFetch.mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockDetailed)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should switch to detail view on item click', async () => {
|
||||||
|
render(Extensions, { onBack, onInstall });
|
||||||
|
const extensionItem = await screen.findByText(mockFeatured.title);
|
||||||
|
await user.click(extensionItem);
|
||||||
|
|
||||||
|
expect(await screen.findByText('Install Extension')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch detailed extension data when an item is selected', async () => {
|
||||||
|
render(Extensions, { onBack, onInstall });
|
||||||
|
const extensionItem = await screen.findByText(mockFeatured.title);
|
||||||
|
await user.click(extensionItem);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockedFetch).toHaveBeenCalledWith(
|
||||||
|
`https://backend.raycast.com/api/v1/extensions/${mockFeatured.author.handle}/${mockFeatured.name}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('4. Actions and Shortcuts', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
extensionsStore.extensions = [
|
||||||
|
mockRegular,
|
||||||
|
createMockExtension('4', 'no-readme', 'Tools', { readme_url: null })
|
||||||
|
];
|
||||||
|
extensionsStore.selectedIndex = 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open extension in browser via action menu', async () => {
|
||||||
|
render(Extensions, { onBack, onInstall });
|
||||||
|
const menuTrigger = await screen.findByTestId('action-menu-trigger');
|
||||||
|
await user.click(menuTrigger);
|
||||||
|
|
||||||
|
const openButton = await screen.findByText('Open in Browser');
|
||||||
|
await user.click(openButton);
|
||||||
|
expect(openUrl).toHaveBeenCalledWith(mockRegular.store_url);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should copy extension URL via action menu', async () => {
|
||||||
|
render(Extensions, { onBack, onInstall });
|
||||||
|
const menuTrigger = await screen.findByTestId('action-menu-trigger');
|
||||||
|
await user.click(menuTrigger);
|
||||||
|
|
||||||
|
const copyButton = await screen.findByText('Copy Extension URL');
|
||||||
|
await user.click(copyButton);
|
||||||
|
expect(mockedClipboard.writeText).toHaveBeenCalledWith(mockRegular.store_url);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should view source code via action menu', async () => {
|
||||||
|
render(Extensions, { onBack, onInstall });
|
||||||
|
const menuTrigger = await screen.findByTestId('action-menu-trigger');
|
||||||
|
await user.click(menuTrigger);
|
||||||
|
|
||||||
|
const sourceButton = await screen.findByText('View Source Code');
|
||||||
|
await user.click(sourceButton);
|
||||||
|
expect(openUrl).toHaveBeenCalledWith(mockRegular.source_url);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable README button if no readme_url exists', async () => {
|
||||||
|
extensionsStore.selectedIndex = 2;
|
||||||
|
render(Extensions, { onBack, onInstall });
|
||||||
|
const menuTrigger = await screen.findByTestId('action-menu-trigger');
|
||||||
|
await waitFor(() => user.click(menuTrigger));
|
||||||
|
|
||||||
|
const readmeButton = await screen.findByText('View README');
|
||||||
|
expect(readmeButton).toHaveAttribute('aria-disabled', 'true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -10,5 +10,9 @@
|
||||||
|
|
||||||
<div class="bg-muted absolute right-0 bottom-0 left-0 h-px"></div>
|
<div class="bg-muted absolute right-0 bottom-0 left-0 h-px"></div>
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<div class="loading-indicator" transition:fade={{ duration: 200 }}></div>
|
<div
|
||||||
|
class="loading-indicator"
|
||||||
|
data-testid="loading-indicator"
|
||||||
|
transition:fade={{ duration: 200 }}
|
||||||
|
></div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
<svelte:window onkeydown={handleKeydown} />
|
<svelte:window onkeydown={handleKeydown} />
|
||||||
|
|
||||||
<DropdownMenu.Root bind:open>
|
<DropdownMenu.Root bind:open>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger data-testid="action-menu-trigger">
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<Button {...props} variant="ghost" size="action">
|
<Button {...props} variant="ghost" size="action">
|
||||||
Actions
|
Actions
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue