Skip to content

Commit 2d38633

Browse files
fix: defects for token/retry/loading UX (#924)
## Summary - tighten token rejection detection so non-token forbidden/policy failures do not clear saved token or trigger token-recovery UX - only show manual strategy-switch recovery when it is actionable (faraday -> browserless) - update converting-state copy to reflect actual behavior: result first, preview after - add regression coverage for non-token 403 behavior ## Scope - Workstream B follow-up only - no broad refactors ## Validation - cd frontend && npm run test:unit -- --run src/__tests__/App.test.tsx ## Notes - docs workspace in this environment has no git remote configured, so only the html2rss-web PR is opened here. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 788ecce commit 2d38633

3 files changed

Lines changed: 62 additions & 4 deletions

File tree

frontend/src/__tests__/App.test.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ describe('App', () => {
276276

277277
expect(screen.getByText('Preparing feed')).toBeInTheDocument();
278278
expect(
279-
screen.getByText('Creating the feed and loading its preview before showing the result.')
279+
screen.getByText('Creating the feed now. The result appears first, then preview loading continues.')
280280
).toBeInTheDocument();
281281
});
282282

@@ -497,6 +497,37 @@ describe('App', () => {
497497
expect(screen.queryByRole('button', { name: /Try .* instead/ })).not.toBeInTheDocument();
498498
});
499499

500+
it('does not treat non-token forbidden failures as token rejection or strategy-recovery UX', async () => {
501+
mockUseAccessToken.mockReturnValue({
502+
token: 'saved-token',
503+
hasToken: true,
504+
saveToken: mockSaveToken,
505+
clearToken: mockClearToken,
506+
isLoading: false,
507+
error: null,
508+
});
509+
mockConvertFeed.mockRejectedValueOnce(
510+
Object.assign(new Error('URL not allowed for this account'), {
511+
manualRetryStrategy: 'browserless',
512+
})
513+
);
514+
515+
render(<App />);
516+
517+
fireEvent.input(screen.getByLabelText('Page URL'), {
518+
target: { value: 'https://example.com/articles' },
519+
});
520+
fireEvent.click(screen.getByRole('button', { name: 'Generate feed URL' }));
521+
522+
await screen.findByText('URL not allowed for this account');
523+
expect(mockClearToken).not.toHaveBeenCalled();
524+
expect(screen.queryByText('Add access token')).not.toBeInTheDocument();
525+
expect(
526+
screen.queryByText('Access token was rejected. Paste a valid token to continue.')
527+
).not.toBeInTheDocument();
528+
expect(screen.queryByRole('button', { name: /Try .* instead/ })).not.toBeInTheDocument();
529+
});
530+
500531
it('shows the utility links in a user-focused order', () => {
501532
window.history.replaceState({}, '', 'http://localhost:3000/#result');
502533
render(<App />);

frontend/src/components/App.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,36 @@ export function App() {
102102

103103
const isAccessTokenError = (message: string) => {
104104
const normalized = message.toLowerCase();
105+
const mentionsAuthToken =
106+
normalized.includes('access token') ||
107+
normalized.includes('token') ||
108+
normalized.includes('authentication') ||
109+
normalized.includes('bearer');
110+
105111
return (
112+
normalized.includes('unauthorized') ||
113+
normalized.includes('invalid token') ||
114+
normalized.includes('token rejected') ||
115+
normalized.includes('authentication') ||
116+
(normalized.includes('forbidden') && mentionsAuthToken)
117+
);
118+
};
119+
120+
const isActionableStrategySwitch = (message: string, currentStrategy: string, retryStrategy: string) => {
121+
if (currentStrategy !== 'faraday' || retryStrategy !== 'browserless') return false;
122+
123+
const normalized = message.toLowerCase();
124+
return !(
106125
normalized.includes('unauthorized') ||
107126
normalized.includes('forbidden') ||
127+
normalized.includes('not allowed') ||
128+
normalized.includes('disabled') ||
108129
normalized.includes('access token') ||
109-
normalized.includes('authentication')
130+
normalized.includes('token') ||
131+
normalized.includes('authentication') ||
132+
normalized.includes('bad request') ||
133+
normalized.includes('url') ||
134+
normalized.includes('unsupported strategy')
110135
);
111136
};
112137

@@ -150,7 +175,9 @@ export function App() {
150175
} catch (submitError) {
151176
const message = submitError instanceof Error ? submitError.message : 'Unable to start feed generation.';
152177
const retryStrategy = (submitError as ConversionErrorWithMeta).manualRetryStrategy ?? '';
153-
setManualRetryStrategy(retryStrategy);
178+
setManualRetryStrategy(
179+
isActionableStrategySwitch(message, strategy, retryStrategy) ? retryStrategy : ''
180+
);
154181

155182
if (feedCreation.access_token_required && isAccessTokenError(message)) {
156183
clearToken();

frontend/src/components/AppPanels.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ export function CreateFeedPanel({
251251
{isConverting && (
252252
<div class="ui-card ui-card--notice ui-card--padded notice" data-state="loading" role="status">
253253
<div class="notice__title">Preparing feed</div>
254-
<p>Creating the feed and loading its preview before showing the result.</p>
254+
<p>Creating the feed now. The result appears first, then preview loading continues.</p>
255255
</div>
256256
)}
257257

0 commit comments

Comments
 (0)