diff --git a/frontend/src/__tests__/App.test.tsx b/frontend/src/__tests__/App.test.tsx index 5d935469..3383146a 100644 --- a/frontend/src/__tests__/App.test.tsx +++ b/frontend/src/__tests__/App.test.tsx @@ -276,7 +276,7 @@ describe('App', () => { expect(screen.getByText('Preparing feed')).toBeInTheDocument(); expect( - screen.getByText('Creating the feed and loading its preview before showing the result.') + screen.getByText('Creating the feed now. The result appears first, then preview loading continues.') ).toBeInTheDocument(); }); @@ -497,6 +497,37 @@ describe('App', () => { expect(screen.queryByRole('button', { name: /Try .* instead/ })).not.toBeInTheDocument(); }); + it('does not treat non-token forbidden failures as token rejection or strategy-recovery UX', async () => { + mockUseAccessToken.mockReturnValue({ + token: 'saved-token', + hasToken: true, + saveToken: mockSaveToken, + clearToken: mockClearToken, + isLoading: false, + error: null, + }); + mockConvertFeed.mockRejectedValueOnce( + Object.assign(new Error('URL not allowed for this account'), { + manualRetryStrategy: 'browserless', + }) + ); + + render(); + + fireEvent.input(screen.getByLabelText('Page URL'), { + target: { value: 'https://example.com/articles' }, + }); + fireEvent.click(screen.getByRole('button', { name: 'Generate feed URL' })); + + await screen.findByText('URL not allowed for this account'); + expect(mockClearToken).not.toHaveBeenCalled(); + expect(screen.queryByText('Add access token')).not.toBeInTheDocument(); + expect( + screen.queryByText('Access token was rejected. Paste a valid token to continue.') + ).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /Try .* instead/ })).not.toBeInTheDocument(); + }); + it('shows the utility links in a user-focused order', () => { window.history.replaceState({}, '', 'http://localhost:3000/#result'); render(); diff --git a/frontend/src/components/App.tsx b/frontend/src/components/App.tsx index 80e6a009..981da692 100644 --- a/frontend/src/components/App.tsx +++ b/frontend/src/components/App.tsx @@ -102,11 +102,36 @@ export function App() { const isAccessTokenError = (message: string) => { const normalized = message.toLowerCase(); + const mentionsAuthToken = + normalized.includes('access token') || + normalized.includes('token') || + normalized.includes('authentication') || + normalized.includes('bearer'); + return ( + normalized.includes('unauthorized') || + normalized.includes('invalid token') || + normalized.includes('token rejected') || + normalized.includes('authentication') || + (normalized.includes('forbidden') && mentionsAuthToken) + ); + }; + + const isActionableStrategySwitch = (message: string, currentStrategy: string, retryStrategy: string) => { + if (currentStrategy !== 'faraday' || retryStrategy !== 'browserless') return false; + + const normalized = message.toLowerCase(); + return !( normalized.includes('unauthorized') || normalized.includes('forbidden') || + normalized.includes('not allowed') || + normalized.includes('disabled') || normalized.includes('access token') || - normalized.includes('authentication') + normalized.includes('token') || + normalized.includes('authentication') || + normalized.includes('bad request') || + normalized.includes('url') || + normalized.includes('unsupported strategy') ); }; @@ -150,7 +175,9 @@ export function App() { } catch (submitError) { const message = submitError instanceof Error ? submitError.message : 'Unable to start feed generation.'; const retryStrategy = (submitError as ConversionErrorWithMeta).manualRetryStrategy ?? ''; - setManualRetryStrategy(retryStrategy); + setManualRetryStrategy( + isActionableStrategySwitch(message, strategy, retryStrategy) ? retryStrategy : '' + ); if (feedCreation.access_token_required && isAccessTokenError(message)) { clearToken(); diff --git a/frontend/src/components/AppPanels.tsx b/frontend/src/components/AppPanels.tsx index 08e1139c..dc8be1ea 100644 --- a/frontend/src/components/AppPanels.tsx +++ b/frontend/src/components/AppPanels.tsx @@ -251,7 +251,7 @@ export function CreateFeedPanel({ {isConverting && (
Preparing feed
-

Creating the feed and loading its preview before showing the result.

+

Creating the feed now. The result appears first, then preview loading continues.

)}