diff --git a/app/web/api/v1/strategies.rb b/app/web/api/v1/strategies.rb index 992211b7..d34f409d 100644 --- a/app/web/api/v1/strategies.rb +++ b/app/web/api/v1/strategies.rb @@ -33,7 +33,7 @@ def index(_request) def display_name_for(name) case name.to_s - when 'faraday' then 'Standard rendering' + when 'faraday' then 'Default' when 'browserless' then 'JavaScript pages (recommended)' else name.to_s.split('_').map(&:capitalize).join(' ') end diff --git a/frontend/index.html b/frontend/index.html index 1324d6e2..e2538ef7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,6 +8,7 @@ name="description" content="html2rss converts fixed demo pages or operator-submitted URLs into feed endpoints." /> + html2rss diff --git a/frontend/src/__tests__/App.contract.test.tsx b/frontend/src/__tests__/App.contract.test.tsx index 1ba9e86a..87d906a6 100644 --- a/frontend/src/__tests__/App.contract.test.tsx +++ b/frontend/src/__tests__/App.contract.test.tsx @@ -64,16 +64,18 @@ describe('App contract', () => { fireEvent.click(screen.getByRole('button', { name: 'Generate feed URL' })); await waitFor(() => { + expect(screen.getByText('Your feed is ready')).toBeInTheDocument(); expect(screen.getByText('Example Feed')).toBeInTheDocument(); expect(screen.getByLabelText('Feed URL')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Copy feed URL' })).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'Open feed' })).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'JSON Feed' })).toHaveAttribute( + expect(screen.getByRole('link', { name: 'Open JSON Feed' })).toHaveAttribute( 'href', 'http://localhost:3000/api/v1/feeds/generated-token.json' ); expect(screen.getByRole('button', { name: 'Create another feed' })).toBeInTheDocument(); - expect(screen.getByText('Feed preview')).toBeInTheDocument(); + expect(screen.getByText('Preview')).toBeInTheDocument(); + expect(screen.getByText('Latest items from this feed')).toBeInTheDocument(); expect(screen.getByText('Contract Item')).toBeInTheDocument(); }); }); diff --git a/frontend/src/__tests__/App.test.tsx b/frontend/src/__tests__/App.test.tsx index 36f032aa..af6bf619 100644 --- a/frontend/src/__tests__/App.test.tsx +++ b/frontend/src/__tests__/App.test.tsx @@ -268,6 +268,36 @@ describe('App', () => { expect(mockClearToken).toHaveBeenCalled(); }); + it('keeps the Docker Hub link before token clear when a token is saved', () => { + mockUseAccessToken.mockReturnValue({ + token: 'saved-token', + hasToken: true, + saveToken: mockSaveToken, + clearToken: mockClearToken, + isLoading: false, + error: null, + }); + + render(); + + fireEvent.click(screen.getByRole('button', { name: 'More' })); + + const utilityItems = Array.from( + screen + .getByLabelText('Utilities') + .querySelectorAll('.utility-strip__items > a, .utility-strip__items > button') + ).map((element) => element.textContent); + + expect(utilityItems).toEqual([ + 'Try included feeds', + 'Bookmarklet', + 'OpenAPI spec', + 'Source code', + 'Install from Docker Hub', + 'Clear saved token', + ]); + }); + it('saves access token and resumes feed creation from the inline prompt', async () => { render(); @@ -375,7 +405,13 @@ describe('App', () => { fireEvent.click(screen.getByRole('button', { name: 'More' })); const utilityLinks = screen.getAllByRole('link').map((link) => link.textContent); - expect(utilityLinks).toEqual(['Try included feeds', 'Bookmarklet', 'OpenAPI spec', 'Source code']); + expect(utilityLinks).toEqual([ + 'Try included feeds', + 'Bookmarklet', + 'OpenAPI spec', + 'Source code', + 'Install from Docker Hub', + ]); expect(screen.getByRole('link', { name: 'OpenAPI spec' })).toHaveAttribute( 'href', @@ -385,5 +421,9 @@ describe('App', () => { 'href', 'https://html2rss.github.io/web-application/how-to/use-included-configs/' ); + expect(screen.getByRole('link', { name: 'Install from Docker Hub' })).toHaveAttribute( + 'href', + 'https://hub.docker.com/r/html2rss/web' + ); }); }); diff --git a/frontend/src/__tests__/ResultDisplay.test.tsx b/frontend/src/__tests__/ResultDisplay.test.tsx index c9ad54a5..b69fd86c 100644 --- a/frontend/src/__tests__/ResultDisplay.test.tsx +++ b/frontend/src/__tests__/ResultDisplay.test.tsx @@ -9,7 +9,7 @@ describe('ResultDisplay', () => { id: 'test-id', name: 'Test Feed', url: 'https://example.com', - strategy: 'ssrf_filter', + strategy: 'faraday', feed_token: 'test-feed-token', public_url: 'https://example.com/feed.xml', json_public_url: 'https://example.com/feed.json', @@ -21,28 +21,44 @@ describe('ResultDisplay', () => { ok: true, json: async () => ({ items: [ - { title: 'Item One' }, - { content_text: '56 points by canpan 1 hour ago | hide | 18 comments' }, - { content_text: '2. Item Two ( example.com )' }, + { + title: 'Item One', + content_text: '

First preview item with markup.

', + url: 'https://example.com/item-one', + date_published: '2024-01-01T00:00:00Z', + }, + { + content_text: '56 points by canpan 1 hour ago | hide | 18 comments', + date_published: '2024-01-02T00:00:00Z', + }, + { + content_text: '2. Item Two ( example.com )', + url: 'https://example.com/item-two', + date_published: '2024-01-03T00:00:00Z', + }, ], }), } as Response); }); - it('renders the simplified result actions and preview', async () => { + it('renders the success state actions and richer preview cards', async () => { render(); + expect(screen.getByText('Your feed is ready')).toBeInTheDocument(); expect(screen.getByText('Test Feed')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Copy feed URL' })).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'Open feed' })).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'JSON Feed' })).toHaveAttribute( + expect(screen.getByRole('link', { name: 'Open JSON Feed' })).toHaveAttribute( 'href', 'https://example.com/feed.json' ); await waitFor(() => { expect(screen.getByText('Item One')).toBeInTheDocument(); + expect(screen.getByText('First preview item with markup.')).toBeInTheDocument(); + expect(screen.getAllByText('Open original')).toHaveLength(2); expect(screen.getByText(/points by canpan/i)).toBeInTheDocument(); expect(screen.getByText('Item Two')).toBeInTheDocument(); + expect(screen.getByText('Latest items from this feed')).toBeInTheDocument(); }); expect(window.fetch).toHaveBeenCalledWith('https://example.com/feed.xml', { headers: { Accept: 'application/feed+json' }, @@ -59,6 +75,7 @@ describe('ResultDisplay', () => { await waitFor(() => { expect(screen.getByText('Preview unavailable right now.')).toBeInTheDocument(); + expect(screen.getByText('Latest items from this feed')).toBeInTheDocument(); }); }); diff --git a/frontend/src/__tests__/useFeedConversion.contract.test.ts b/frontend/src/__tests__/useFeedConversion.contract.test.ts index 3c46604d..a7f51a17 100644 --- a/frontend/src/__tests__/useFeedConversion.contract.test.ts +++ b/frontend/src/__tests__/useFeedConversion.contract.test.ts @@ -13,7 +13,7 @@ describe('useFeedConversion contract', () => { const body = (await request.json()) as { url: string; strategy: string }; receivedAuthorization = request.headers.get('authorization'); - expect(body).toEqual({ url: 'https://example.com/articles', strategy: 'ssrf_filter' }); + expect(body).toEqual({ url: 'https://example.com/articles', strategy: 'faraday' }); return HttpResponse.json( buildFeedResponse({ @@ -30,7 +30,7 @@ describe('useFeedConversion contract', () => { const { result } = renderHook(() => useFeedConversion()); await act(async () => { - await result.current.convertFeed('https://example.com/articles', 'ssrf_filter', 'test-token-123'); + await result.current.convertFeed('https://example.com/articles', 'faraday', 'test-token-123'); }); expect(receivedAuthorization).toBe('Bearer test-token-123'); @@ -54,7 +54,7 @@ describe('useFeedConversion contract', () => { await act(async () => { await expect( - result.current.convertFeed('https://example.com/articles', 'ssrf_filter', 'token') + result.current.convertFeed('https://example.com/articles', 'faraday', 'token') ).rejects.toThrow('URL parameter is required'); }); @@ -76,7 +76,7 @@ describe('useFeedConversion contract', () => { await act(async () => { await expect( - result.current.convertFeed('https://example.com/articles', 'ssrf_filter', 'token') + result.current.convertFeed('https://example.com/articles', 'faraday', 'token') ).rejects.toThrow('Invalid response format from feed creation API'); }); diff --git a/frontend/src/__tests__/useFeedConversion.test.ts b/frontend/src/__tests__/useFeedConversion.test.ts index c2583985..9fe72176 100644 --- a/frontend/src/__tests__/useFeedConversion.test.ts +++ b/frontend/src/__tests__/useFeedConversion.test.ts @@ -27,7 +27,7 @@ describe('useFeedConversion', () => { id: 'test-id', name: 'Test Feed', url: 'https://example.com', - strategy: 'ssrf_filter', + strategy: 'faraday', feed_token: 'test-token', public_url: 'https://example.com/feed.xml', json_public_url: 'https://example.com/feed.json', @@ -51,7 +51,7 @@ describe('useFeedConversion', () => { const { result } = renderHook(() => useFeedConversion()); await act(async () => { - await result.current.convertFeed('https://example.com', 'ssrf_filter', 'testtoken'); + await result.current.convertFeed('https://example.com', 'faraday', 'testtoken'); }); expect(result.current.isConverting).toBe(false); @@ -77,9 +77,9 @@ describe('useFeedConversion', () => { const { result } = renderHook(() => useFeedConversion()); await act(async () => { - await expect( - result.current.convertFeed('https://example.com', 'ssrf_filter', 'testtoken') - ).rejects.toThrow('Bad Request'); + await expect(result.current.convertFeed('https://example.com', 'faraday', 'testtoken')).rejects.toThrow( + 'Bad Request' + ); }); expect(result.current.isConverting).toBe(false); @@ -93,9 +93,9 @@ describe('useFeedConversion', () => { const { result } = renderHook(() => useFeedConversion()); await act(async () => { - await expect( - result.current.convertFeed('https://example.com', 'ssrf_filter', 'testtoken') - ).rejects.toThrow('Network error'); + await expect(result.current.convertFeed('https://example.com', 'faraday', 'testtoken')).rejects.toThrow( + 'Network error' + ); }); expect(result.current.isConverting).toBe(false); diff --git a/frontend/src/components/AppPanels.tsx b/frontend/src/components/AppPanels.tsx index b622387a..59a8fd95 100644 --- a/frontend/src/components/AppPanels.tsx +++ b/frontend/src/components/AppPanels.tsx @@ -292,6 +292,14 @@ export function UtilityStrip({ > Source code + + Install from Docker Hub + {hasAccessToken && (