Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/web/api/v1/strategies.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
name="description"
content="html2rss converts fixed demo pages or operator-submitted URLs into feed endpoints."
/>
<link rel="stylesheet" href="/shared-ui.css" />
<link rel="icon" href="/favicon.ico" />
<title>html2rss</title>
</head>
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/__tests__/App.contract.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Expand Down
42 changes: 41 additions & 1 deletion frontend/src/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<App />);

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(<App />);

Expand Down Expand Up @@ -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',
Expand All @@ -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'
);
});
});
29 changes: 23 additions & 6 deletions frontend/src/__tests__/ResultDisplay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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&nbsp;comments' },
{ content_text: '2. Item Two ( example.com )' },
{
title: 'Item One',
content_text: '<p>First preview item with <strong>markup</strong>.</p>',
url: 'https://example.com/item-one',
date_published: '2024-01-01T00:00:00Z',
},
{
content_text: '56 points by canpan 1 hour ago | hide | 18&nbsp;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(<ResultDisplay result={mockResult} onCreateAnother={mockOnCreateAnother} />);

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' },
Expand All @@ -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();
});
});

Expand Down
8 changes: 4 additions & 4 deletions frontend/src/__tests__/useFeedConversion.contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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');
Expand All @@ -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');
});

Expand All @@ -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');
});

Expand Down
16 changes: 8 additions & 8 deletions frontend/src/__tests__/useFeedConversion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/components/AppPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,14 @@ export function UtilityStrip({
>
Source code
</a>
<a
href="https://hub.docker.com/r/html2rss/web"
target="_blank"
rel="noopener noreferrer"
class="utility-link"
>
Install from Docker Hub
</a>
{hasAccessToken && (
<button type="button" class="utility-button" onClick={onClearToken}>
Clear saved token
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/components/DominantField.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { JSX, Ref } from 'preact';

interface DominantFieldProps {
className?: string;
id: string;
label: string;
value: string;
Expand All @@ -19,6 +20,7 @@ interface DominantFieldProps {
}

export function DominantField({
className,
id,
label,
value,
Expand All @@ -36,14 +38,14 @@ export function DominantField({
error,
}: DominantFieldProps) {
return (
<div class="dominant-field">
<label class="field-block field-block--primary field-block--hero" htmlFor={id}>
<div class={className ? `dominant-field ${className}` : 'dominant-field'}>
<label class="field-block field-block--centered" htmlFor={id}>
<span class="field-label field-label--ghost">{label}</span>
<input
id={id}
name={id}
type={type}
class="input input--mono input--hero"
class="input input--mono input--lg"
placeholder={placeholder}
autocomplete={type === 'url' ? 'url' : 'off'}
autoFocus={autoFocus}
Expand Down
Loading
Loading