@@ -91,11 +91,22 @@ describe('App', () => {
9191 render ( < App /> ) ;
9292
9393 expect ( screen . getByLabelText ( 'html2rss' ) ) . toBeInTheDocument ( ) ;
94+ expect ( screen . getByRole ( 'link' , { name : 'html2rss' } ) ) . toHaveAttribute ( 'href' , '/' ) ;
9495 expect ( screen . getByLabelText ( 'Page URL' ) ) . toBeInTheDocument ( ) ;
9596 expect ( screen . getByRole ( 'button' , { name : 'More' } ) ) . toBeInTheDocument ( ) ;
9697 expect ( screen . queryByRole ( 'link' , { name : 'Bookmarklet' } ) ) . not . toBeInTheDocument ( ) ;
9798 } ) ;
9899
100+ it ( 'keeps the page url field permissive enough for hostname-only input' , ( ) => {
101+ render ( < App /> ) ;
102+
103+ const urlInput = screen . getByLabelText ( 'Page URL' ) ;
104+
105+ expect ( urlInput ) . toHaveAttribute ( 'type' , 'text' ) ;
106+ expect ( urlInput ) . toHaveAttribute ( 'inputmode' , 'url' ) ;
107+ expect ( urlInput ) . toHaveAttribute ( 'autocapitalize' , 'off' ) ;
108+ } ) ;
109+
99110 it ( 'autofocuses the source url field' , async ( ) => {
100111 render ( < App /> ) ;
101112
@@ -104,11 +115,11 @@ describe('App', () => {
104115 } ) ;
105116 } ) ;
106117
107- it ( 'prefers browserless as the default strategy when available' , ( ) => {
118+ it ( 'prefers faraday as the default strategy when available' , ( ) => {
108119 render ( < App /> ) ;
109120
110121 return waitFor ( ( ) => {
111- expect ( screen . getByRole ( 'combobox' ) ) . toHaveValue ( 'browserless ' ) ;
122+ expect ( screen . getByRole ( 'combobox' ) ) . toHaveValue ( 'faraday ' ) ;
112123 } ) ;
113124 } ) ;
114125
@@ -140,11 +151,7 @@ describe('App', () => {
140151 render ( < App /> ) ;
141152
142153 await waitFor ( ( ) => {
143- expect ( mockConvertFeed ) . toHaveBeenCalledWith (
144- 'https://example.com/articles' ,
145- 'browserless' ,
146- 'saved-token'
147- ) ;
154+ expect ( mockConvertFeed ) . toHaveBeenCalledWith ( 'https://example.com/articles' , 'faraday' , 'saved-token' ) ;
148155 } ) ;
149156 } ) ;
150157
@@ -221,7 +228,9 @@ describe('App', () => {
221228 preview : {
222229 items : [ ] ,
223230 error : 'Preview unavailable right now.' ,
231+ isLoading : false ,
224232 } ,
233+ retry : null ,
225234 } ,
226235 error : null ,
227236 convertFeed : mockConvertFeed ,
@@ -243,6 +252,7 @@ describe('App', () => {
243252 result : null ,
244253 error : 'Access denied' ,
245254 convertFeed : mockConvertFeed ,
255+ clearError : mockClearConversionError ,
246256 clearResult : mockClearResult ,
247257 } ) ;
248258
@@ -331,11 +341,7 @@ describe('App', () => {
331341
332342 await waitFor ( ( ) => {
333343 expect ( mockSaveToken ) . toHaveBeenCalledWith ( 'token-123' ) ;
334- expect ( mockConvertFeed ) . toHaveBeenCalledWith (
335- 'https://example.com/articles' ,
336- 'browserless' ,
337- 'token-123'
338- ) ;
344+ expect ( mockConvertFeed ) . toHaveBeenCalledWith ( 'https://example.com/articles' , 'faraday' , 'token-123' ) ;
339345 } ) ;
340346 } ) ;
341347
@@ -419,13 +425,87 @@ describe('App', () => {
419425 expect ( bookmarklet . getAttribute ( 'href' ) ) . not . toContain ( '%27+encodeURIComponent' ) ;
420426 } ) ;
421427
428+ it ( 'opens token entry immediately for bookmarklet urls when no token is saved' , async ( ) => {
429+ window . history . replaceState ( { } , '' , 'http://localhost:3000/?url=example.com%2Farticles' ) ;
430+
431+ render ( < App /> ) ;
432+
433+ await screen . findByText ( 'Add access token' ) ;
434+ expect ( screen . getByLabelText ( 'Page URL' ) ) . toHaveValue ( 'https://example.com/articles' ) ;
435+ expect ( mockConvertFeed ) . not . toHaveBeenCalled ( ) ;
436+ } ) ;
437+
438+ it ( 'offers a direct alternate strategy retry after conversion failure' , async ( ) => {
439+ mockUseAccessToken . mockReturnValue ( {
440+ token : 'saved-token' ,
441+ hasToken : true ,
442+ saveToken : mockSaveToken ,
443+ clearToken : mockClearToken ,
444+ isLoading : false ,
445+ error : null ,
446+ } ) ;
447+ mockConvertFeed
448+ . mockRejectedValueOnce (
449+ Object . assign ( new Error ( 'Tried faraday first, then browserless. Browserless failed.' ) , {
450+ manualRetryStrategy : 'browserless' ,
451+ } )
452+ )
453+ . mockResolvedValueOnce ( undefined ) ;
454+
455+ render ( < App /> ) ;
456+
457+ fireEvent . input ( screen . getByLabelText ( 'Page URL' ) , {
458+ target : { value : 'https://example.com/articles' } ,
459+ } ) ;
460+ fireEvent . click ( screen . getByRole ( 'button' , { name : 'Generate feed URL' } ) ) ;
461+
462+ await screen . findByRole ( 'button' , { name : 'Try browserless instead' } ) ;
463+ fireEvent . click ( screen . getByRole ( 'button' , { name : 'Try browserless instead' } ) ) ;
464+
465+ await waitFor ( ( ) => {
466+ expect ( mockConvertFeed ) . toHaveBeenLastCalledWith (
467+ 'https://example.com/articles' ,
468+ 'browserless' ,
469+ 'saved-token'
470+ ) ;
471+ } ) ;
472+ } ) ;
473+
474+ it ( 'does not offer a duplicate retry action after automatic fallback already failed' , async ( ) => {
475+ mockUseAccessToken . mockReturnValue ( {
476+ token : 'saved-token' ,
477+ hasToken : true ,
478+ saveToken : mockSaveToken ,
479+ clearToken : mockClearToken ,
480+ isLoading : false ,
481+ error : null ,
482+ } ) ;
483+ mockConvertFeed . mockRejectedValueOnce (
484+ Object . assign ( new Error ( 'Tried faraday first, then browserless. Browserless failed.' ) , {
485+ manualRetryStrategy : '' ,
486+ } )
487+ ) ;
488+
489+ render ( < App /> ) ;
490+
491+ fireEvent . input ( screen . getByLabelText ( 'Page URL' ) , {
492+ target : { value : 'https://example.com/articles' } ,
493+ } ) ;
494+ fireEvent . click ( screen . getByRole ( 'button' , { name : 'Generate feed URL' } ) ) ;
495+
496+ await screen . findByText ( 'Tried faraday first, then browserless. Browserless failed.' ) ;
497+ expect ( screen . queryByRole ( 'button' , { name : / T r y .* i n s t e a d / } ) ) . not . toBeInTheDocument ( ) ;
498+ } ) ;
499+
422500 it ( 'shows the utility links in a user-focused order' , ( ) => {
423501 window . history . replaceState ( { } , '' , 'http://localhost:3000/#result' ) ;
424502 render ( < App /> ) ;
425503
426504 fireEvent . click ( screen . getByRole ( 'button' , { name : 'More' } ) ) ;
427505
428- const utilityLinks = screen . getAllByRole ( 'link' ) . map ( ( link ) => link . textContent ) ;
506+ const utilityLinks = Array . from (
507+ screen . getByLabelText ( 'Utilities' ) . querySelectorAll ( '.utility-strip__items > a' )
508+ ) . map ( ( link ) => link . textContent ) ;
429509 expect ( utilityLinks ) . toEqual ( [
430510 'Try included feeds' ,
431511 'Bookmarklet' ,
0 commit comments