Skip to content

Commit 46af096

Browse files
committed
feat: make readiness/preview checks frontend-owned
1 parent 97a42db commit 46af096

19 files changed

Lines changed: 429 additions & 1292 deletions

frontend/e2e/smoke.spec.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ test.describe('frontend smoke', () => {
4040
await expect(page.getByRole('button', { name: 'Back' })).toBeVisible();
4141

4242
await page.getByRole('button', { name: 'Back' }).click();
43-
await expect(page).toHaveURL(/\/create(?:\?.*)?$/);
43+
await expect(page).toHaveURL(/#\/create(?:\?.*)?$/);
4444
await expect(page.getByRole('button', { name: 'Generate feed URL' })).toBeVisible();
4545
await expect(page.locator('.form-shell')).toHaveAttribute('data-state', 'idle');
4646
});
@@ -97,15 +97,14 @@ test.describe('frontend smoke', () => {
9797
],
9898
isLoading: false,
9999
},
100-
readinessPhase: 'feed_ready',
101-
previewStatus: 'ready',
100+
workflowState: 'preview_ready' as const,
102101
warnings: [],
103102
},
104103
})
105104
);
106105
});
107106

108-
await page.goto('/result/generated-token');
107+
await page.goto('/#/result/generated-token');
109108

110109
await expect(page.getByRole('heading', { name: 'Feed ready' })).toBeVisible();
111110
await expect(page.locator('.result-shell')).toHaveAttribute('data-state', 'ready');
@@ -121,13 +120,11 @@ test.describe('frontend smoke', () => {
121120
localStorage.removeItem('html2rss_feed_result_snapshot:missing-token');
122121
});
123122

124-
await page.goto('/result/missing-token');
123+
await page.goto('/#/result/missing-token');
125124

126125
await expect(page.getByText('Saved result unavailable')).toBeVisible();
127-
await expect(
128-
page.getByText('We could not restore this feed result. Create a new feed link to continue.')
129-
).toBeVisible();
126+
await expect(page.getByText('Create a new feed link to continue.')).toBeVisible();
130127
await expect(page.getByRole('button', { name: 'Go to create' })).toBeVisible();
131-
await expect(page.locator('.notice')).toHaveAttribute('data-tone', 'error');
128+
await expect(page.locator('.result-recovery')).toHaveAttribute('data-state', 'failed');
132129
});
133130
});

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

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, beforeEach } from 'vitest';
1+
import { describe, it, expect, beforeEach, vi } from 'vitest';
22
import { render, screen, fireEvent, waitFor } from '@testing-library/preact';
33
import { http, HttpResponse } from 'msw';
44
import { server, buildFeedResponse, buildStructuredErrorResponse } from './mocks/server';
@@ -8,13 +8,37 @@ describe('App contract', () => {
88
const token = 'contract-token';
99

1010
beforeEach(() => {
11-
globalThis.history.replaceState({}, '', 'http://localhost:3000/create');
11+
globalThis.history.replaceState({}, '', 'http://localhost:3000/#/create');
1212
globalThis.localStorage.clear();
1313
globalThis.sessionStorage.clear();
1414
globalThis.sessionStorage.setItem('html2rss_access_token', token);
1515
});
1616

17-
it('shows feed result when the API returns structured create and status payloads', async () => {
17+
it('shows feed result when the API returns structured create payload and preview feed', async () => {
18+
const nativeFetch = globalThis.fetch;
19+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation((input, init) => {
20+
if (String(input).endsWith('/api/v1/feeds/generated-token.json')) {
21+
expect((init?.headers as Record<string, string> | undefined)?.Accept).toBe('application/feed+json');
22+
return Promise.resolve(
23+
new Response(
24+
JSON.stringify({
25+
items: [
26+
{
27+
title: 'Contract Item',
28+
content_text: 'Contract preview excerpt.',
29+
url: 'https://example.com/contract-item',
30+
date_published: '2024-01-01T00:00:00Z',
31+
},
32+
],
33+
}),
34+
{ status: 200, headers: { 'Content-Type': 'application/feed+json' } }
35+
)
36+
);
37+
}
38+
39+
return nativeFetch(input, init);
40+
});
41+
1842
server.use(
1943
http.post('/api/v1/feeds', async ({ request }) => {
2044
const body = (await request.json()) as { url: string };
@@ -28,30 +52,11 @@ describe('App contract', () => {
2852
feed_token: 'generated-token',
2953
public_url: '/api/v1/feeds/generated-token',
3054
json_public_url: '/api/v1/feeds/generated-token.json',
31-
conversion: {
32-
readiness_phase: 'link_created',
33-
preview_status: 'pending',
34-
warnings: [],
35-
},
3655
}),
3756
{ status: 201 }
3857
);
3958
}),
40-
http.get('/api/v1/feeds/generated-token/status', () =>
41-
HttpResponse.json(
42-
buildFeedResponse({
43-
feed_token: 'generated-token',
44-
public_url: '/api/v1/feeds/generated-token',
45-
json_public_url: '/api/v1/feeds/generated-token.json',
46-
conversion: {
47-
readiness_phase: 'feed_ready',
48-
preview_status: 'ready',
49-
warnings: [],
50-
},
51-
})
52-
)
53-
),
54-
http.get('/api/v1/feeds/generated-token.json', ({ request }) => {
59+
http.get('http://localhost:3000/api/v1/feeds/generated-token.json', ({ request }) => {
5560
expect(request.headers.get('accept')).toBe('application/feed+json');
5661

5762
return HttpResponse.json(
@@ -69,6 +74,20 @@ describe('App contract', () => {
6974
headers: { 'content-type': 'application/feed+json' },
7075
}
7176
);
77+
}),
78+
http.get('/api/v1/feeds/generated-token.json', ({ request }) => {
79+
expect(request.headers.get('accept')).toBe('application/feed+json');
80+
81+
return HttpResponse.json({
82+
items: [
83+
{
84+
title: 'Contract Item',
85+
content_text: 'Contract preview excerpt.',
86+
url: 'https://example.com/contract-item',
87+
date_published: '2024-01-01T00:00:00Z',
88+
},
89+
],
90+
});
7291
})
7392
);
7493

@@ -92,6 +111,7 @@ describe('App contract', () => {
92111
expect(screen.getByRole('button', { name: 'Create another feed' })).toBeInTheDocument();
93112
expect(screen.getByText('Latest items from this feed')).toBeInTheDocument();
94113
});
114+
fetchSpy.mockRestore();
95115
});
96116

97117
it('reopens token recovery when a saved token is rejected by structured auth metadata', async () => {

0 commit comments

Comments
 (0)