Skip to content

Commit 45745c9

Browse files
committed
feat: unify web and feed result surfaces
1 parent 377cff0 commit 45745c9

13 files changed

Lines changed: 893 additions & 378 deletions

app/web/api/v1/strategies.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def index(_request)
3333

3434
def display_name_for(name)
3535
case name.to_s
36-
when 'faraday' then 'Standard rendering'
36+
when 'faraday' then 'Default'
3737
when 'browserless' then 'JavaScript pages (recommended)'
3838
else name.to_s.split('_').map(&:capitalize).join(' ')
3939
end

frontend/e2e/smoke.spec.ts

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,13 @@ import { expect, test } from '@playwright/test';
22

33
test.describe('frontend smoke', () => {
44
test('loads create flow and inline access-token gate', async ({ page }) => {
5-
await page.route(/\/api\/v1$/, async (route) => {
6-
await route.fulfill({
7-
status: 200,
8-
contentType: 'application/json',
9-
body: JSON.stringify({
10-
success: true,
11-
data: {
12-
api: {
13-
name: 'html2rss-web API',
14-
description: 'RESTful API for converting websites to RSS feeds',
15-
openapi_url: 'http://example.test/openapi.yaml',
16-
},
17-
instance: {
18-
feed_creation: {
19-
enabled: true,
20-
access_token_required: true,
21-
},
22-
featured_feeds: [],
23-
},
24-
},
25-
}),
26-
});
27-
});
28-
29-
await page.route(/\/api\/v1\/strategies$/, async (route) => {
30-
await route.fulfill({
31-
status: 200,
32-
contentType: 'application/json',
33-
body: JSON.stringify({
34-
success: true,
35-
data: {
36-
strategies: [
37-
{ id: 'faraday', name: 'faraday', display_name: 'Default' },
38-
{
39-
id: 'browserless',
40-
name: 'browserless',
41-
display_name: 'JavaScript pages (recommended)',
42-
},
43-
],
44-
},
45-
meta: { total: 2 },
46-
}),
47-
});
48-
});
49-
505
await page.goto('/');
516

52-
await expect(page.getByLabel('Page URL')).toBeVisible();
7+
await expect(page.getByLabel('PAGE URL')).toBeVisible();
538
await expect(page.getByRole('button', { name: 'Generate feed URL' })).toBeVisible();
54-
await expect(page.getByRole('button', { name: 'More' })).toBeVisible();
9+
await expect(page.getByRole('button', { name: 'MORE' })).toBeVisible();
5510

56-
await page.getByLabel('Page URL').fill('https://example.com/articles');
11+
await page.getByLabel('PAGE URL').fill('https://example.com/articles');
5712
await page.getByRole('button', { name: 'Generate feed URL' }).click();
5813

5914
await expect(page.getByRole('heading', { name: 'Add access token' })).toBeVisible();
@@ -63,6 +18,6 @@ test.describe('frontend smoke', () => {
6318

6419
await page.getByRole('button', { name: 'Back' }).click();
6520
await expect(page.getByRole('button', { name: 'Generate feed URL' })).toBeVisible();
66-
await expect(page.getByRole('button', { name: 'More' })).toBeVisible();
21+
await expect(page.getByRole('button', { name: 'MORE' })).toBeVisible();
6722
});
6823
});

frontend/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
name="description"
99
content="html2rss converts fixed demo pages or operator-submitted URLs into feed endpoints."
1010
/>
11+
<link rel="stylesheet" href="/shared-ui.css" />
1112
<link rel="icon" href="/favicon.ico" />
1213
<title>html2rss</title>
1314
</head>

frontend/src/__tests__/App.contract.test.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,18 @@ describe('App contract', () => {
6464
fireEvent.click(screen.getByRole('button', { name: 'Generate feed URL' }));
6565

6666
await waitFor(() => {
67+
expect(screen.getByText('Your feed is ready')).toBeInTheDocument();
6768
expect(screen.getByText('Example Feed')).toBeInTheDocument();
6869
expect(screen.getByLabelText('Feed URL')).toBeInTheDocument();
6970
expect(screen.getByRole('button', { name: 'Copy feed URL' })).toBeInTheDocument();
7071
expect(screen.getByRole('link', { name: 'Open feed' })).toBeInTheDocument();
71-
expect(screen.getByRole('link', { name: 'JSON Feed' })).toHaveAttribute(
72+
expect(screen.getByRole('link', { name: 'Open JSON Feed' })).toHaveAttribute(
7273
'href',
7374
'http://localhost:3000/api/v1/feeds/generated-token.json'
7475
);
7576
expect(screen.getByRole('button', { name: 'Create another feed' })).toBeInTheDocument();
76-
expect(screen.getByText('Feed preview')).toBeInTheDocument();
77+
expect(screen.getByText('Preview')).toBeInTheDocument();
78+
expect(screen.getByText('Latest items from this feed')).toBeInTheDocument();
7779
expect(screen.getByText('Contract Item')).toBeInTheDocument();
7880
});
7981
});

frontend/src/__tests__/ResultDisplay.test.tsx

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe('ResultDisplay', () => {
99
id: 'test-id',
1010
name: 'Test Feed',
1111
url: 'https://example.com',
12-
strategy: 'ssrf_filter',
12+
strategy: 'faraday',
1313
feed_token: 'test-feed-token',
1414
public_url: 'https://example.com/feed.xml',
1515
json_public_url: 'https://example.com/feed.json',
@@ -21,28 +21,44 @@ describe('ResultDisplay', () => {
2121
ok: true,
2222
json: async () => ({
2323
items: [
24-
{ title: 'Item One' },
25-
{ content_text: '56 points by canpan 1 hour ago | hide | 18&nbsp;comments' },
26-
{ content_text: '2. Item Two ( example.com )' },
24+
{
25+
title: 'Item One',
26+
content_text: '<p>First preview item with <strong>markup</strong>.</p>',
27+
url: 'https://example.com/item-one',
28+
date_published: '2024-01-01T00:00:00Z',
29+
},
30+
{
31+
content_text: '56 points by canpan 1 hour ago | hide | 18&nbsp;comments',
32+
date_published: '2024-01-02T00:00:00Z',
33+
},
34+
{
35+
content_text: '2. Item Two ( example.com )',
36+
url: 'https://example.com/item-two',
37+
date_published: '2024-01-03T00:00:00Z',
38+
},
2739
],
2840
}),
2941
} as Response);
3042
});
3143

32-
it('renders the simplified result actions and preview', async () => {
44+
it('renders the success state actions and richer preview cards', async () => {
3345
render(<ResultDisplay result={mockResult} onCreateAnother={mockOnCreateAnother} />);
3446

47+
expect(screen.getByText('Your feed is ready')).toBeInTheDocument();
3548
expect(screen.getByText('Test Feed')).toBeInTheDocument();
3649
expect(screen.getByRole('button', { name: 'Copy feed URL' })).toBeInTheDocument();
3750
expect(screen.getByRole('link', { name: 'Open feed' })).toBeInTheDocument();
38-
expect(screen.getByRole('link', { name: 'JSON Feed' })).toHaveAttribute(
51+
expect(screen.getByRole('link', { name: 'Open JSON Feed' })).toHaveAttribute(
3952
'href',
4053
'https://example.com/feed.json'
4154
);
4255
await waitFor(() => {
4356
expect(screen.getByText('Item One')).toBeInTheDocument();
57+
expect(screen.getByText('First preview item with markup.')).toBeInTheDocument();
58+
expect(screen.getAllByText('Open original')).toHaveLength(2);
4459
expect(screen.getByText(/points by canpan/i)).toBeInTheDocument();
4560
expect(screen.getByText('Item Two')).toBeInTheDocument();
61+
expect(screen.getByText('Latest items from this feed')).toBeInTheDocument();
4662
});
4763
expect(window.fetch).toHaveBeenCalledWith('https://example.com/feed.xml', {
4864
headers: { Accept: 'application/feed+json' },
@@ -59,6 +75,7 @@ describe('ResultDisplay', () => {
5975

6076
await waitFor(() => {
6177
expect(screen.getByText('Preview unavailable right now.')).toBeInTheDocument();
78+
expect(screen.getByText('Latest items from this feed')).toBeInTheDocument();
6279
});
6380
});
6481

frontend/src/__tests__/useFeedConversion.contract.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe('useFeedConversion contract', () => {
1313
const body = (await request.json()) as { url: string; strategy: string };
1414
receivedAuthorization = request.headers.get('authorization');
1515

16-
expect(body).toEqual({ url: 'https://example.com/articles', strategy: 'ssrf_filter' });
16+
expect(body).toEqual({ url: 'https://example.com/articles', strategy: 'faraday' });
1717

1818
return HttpResponse.json(
1919
buildFeedResponse({
@@ -30,7 +30,7 @@ describe('useFeedConversion contract', () => {
3030
const { result } = renderHook(() => useFeedConversion());
3131

3232
await act(async () => {
33-
await result.current.convertFeed('https://example.com/articles', 'ssrf_filter', 'test-token-123');
33+
await result.current.convertFeed('https://example.com/articles', 'faraday', 'test-token-123');
3434
});
3535

3636
expect(receivedAuthorization).toBe('Bearer test-token-123');
@@ -54,7 +54,7 @@ describe('useFeedConversion contract', () => {
5454

5555
await act(async () => {
5656
await expect(
57-
result.current.convertFeed('https://example.com/articles', 'ssrf_filter', 'token')
57+
result.current.convertFeed('https://example.com/articles', 'faraday', 'token')
5858
).rejects.toThrow('URL parameter is required');
5959
});
6060

@@ -76,7 +76,7 @@ describe('useFeedConversion contract', () => {
7676

7777
await act(async () => {
7878
await expect(
79-
result.current.convertFeed('https://example.com/articles', 'ssrf_filter', 'token')
79+
result.current.convertFeed('https://example.com/articles', 'faraday', 'token')
8080
).rejects.toThrow('Invalid response format from feed creation API');
8181
});
8282

frontend/src/__tests__/useFeedConversion.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe('useFeedConversion', () => {
2727
id: 'test-id',
2828
name: 'Test Feed',
2929
url: 'https://example.com',
30-
strategy: 'ssrf_filter',
30+
strategy: 'faraday',
3131
feed_token: 'test-token',
3232
public_url: 'https://example.com/feed.xml',
3333
json_public_url: 'https://example.com/feed.json',
@@ -51,7 +51,7 @@ describe('useFeedConversion', () => {
5151
const { result } = renderHook(() => useFeedConversion());
5252

5353
await act(async () => {
54-
await result.current.convertFeed('https://example.com', 'ssrf_filter', 'testtoken');
54+
await result.current.convertFeed('https://example.com', 'faraday', 'testtoken');
5555
});
5656

5757
expect(result.current.isConverting).toBe(false);
@@ -77,9 +77,9 @@ describe('useFeedConversion', () => {
7777
const { result } = renderHook(() => useFeedConversion());
7878

7979
await act(async () => {
80-
await expect(
81-
result.current.convertFeed('https://example.com', 'ssrf_filter', 'testtoken')
82-
).rejects.toThrow('Bad Request');
80+
await expect(result.current.convertFeed('https://example.com', 'faraday', 'testtoken')).rejects.toThrow(
81+
'Bad Request'
82+
);
8383
});
8484

8585
expect(result.current.isConverting).toBe(false);
@@ -93,9 +93,9 @@ describe('useFeedConversion', () => {
9393
const { result } = renderHook(() => useFeedConversion());
9494

9595
await act(async () => {
96-
await expect(
97-
result.current.convertFeed('https://example.com', 'ssrf_filter', 'testtoken')
98-
).rejects.toThrow('Network error');
96+
await expect(result.current.convertFeed('https://example.com', 'faraday', 'testtoken')).rejects.toThrow(
97+
'Network error'
98+
);
9999
});
100100

101101
expect(result.current.isConverting).toBe(false);

frontend/src/components/DominantField.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { JSX, Ref } from 'preact';
22

33
interface DominantFieldProps {
4+
className?: string;
45
id: string;
56
label: string;
67
value: string;
@@ -19,6 +20,7 @@ interface DominantFieldProps {
1920
}
2021

2122
export function DominantField({
23+
className,
2224
id,
2325
label,
2426
value,
@@ -36,14 +38,14 @@ export function DominantField({
3638
error,
3739
}: DominantFieldProps) {
3840
return (
39-
<div class="dominant-field">
40-
<label class="field-block field-block--primary field-block--hero" htmlFor={id}>
41+
<div class={className ? `dominant-field ${className}` : 'dominant-field'}>
42+
<label class="field-block field-block--centered" htmlFor={id}>
4143
<span class="field-label field-label--ghost">{label}</span>
4244
<input
4345
id={id}
4446
name={id}
4547
type={type}
46-
class="input input--mono input--hero"
48+
class="input input--mono input--lg"
4749
placeholder={placeholder}
4850
autocomplete={type === 'url' ? 'url' : 'off'}
4951
autoFocus={autoFocus}

0 commit comments

Comments
 (0)