@@ -38,6 +38,7 @@ describe('App', () => {
3838
3939 beforeEach ( ( ) => {
4040 vi . clearAllMocks ( ) ;
41+ window . history . replaceState ( { } , '' , 'http://localhost:3000/' ) ;
4142
4243 mockUseAccessToken . mockReturnValue ( {
4344 token : null ,
@@ -60,6 +61,7 @@ describe('App', () => {
6061 enabled : true ,
6162 access_token_required : true ,
6263 } ,
64+ featured_feeds : [ ] ,
6365 } ,
6466 } ,
6567 isLoading : false ,
@@ -77,8 +79,8 @@ describe('App', () => {
7779
7880 mockUseStrategies . mockReturnValue ( {
7981 strategies : [
80- { id : 'ssrf_filter ' , name : 'ssrf_filter ' , display_name : 'Standard (recommended) ' } ,
81- { id : 'browserless' , name : 'browserless' , display_name : 'JavaScript pages' } ,
82+ { id : 'faraday ' , name : 'faraday ' , display_name : 'Default ' } ,
83+ { id : 'browserless' , name : 'browserless' , display_name : 'JavaScript pages (recommended) ' } ,
8284 ] ,
8385 isLoading : false ,
8486 error : null ,
@@ -102,6 +104,50 @@ describe('App', () => {
102104 } ) ;
103105 } ) ;
104106
107+ it ( 'prefers browserless as the default strategy when available' , ( ) => {
108+ render ( < App /> ) ;
109+
110+ return waitFor ( ( ) => {
111+ expect ( screen . getByRole ( 'combobox' ) ) . toHaveValue ( 'browserless' ) ;
112+ } ) ;
113+ } ) ;
114+
115+ it ( 'falls back to the first available strategy when browserless is unavailable' , ( ) => {
116+ mockUseStrategies . mockReturnValue ( {
117+ strategies : [ { id : 'faraday' , name : 'faraday' , display_name : 'Default' } ] ,
118+ isLoading : false ,
119+ error : null ,
120+ } ) ;
121+
122+ render ( < App /> ) ;
123+
124+ return waitFor ( ( ) => {
125+ expect ( screen . getByRole ( 'combobox' ) ) . toHaveValue ( 'faraday' ) ;
126+ } ) ;
127+ } ) ;
128+
129+ it ( 'auto-submits a prefilled url using the resolved default strategy' , async ( ) => {
130+ mockUseAccessToken . mockReturnValue ( {
131+ token : 'saved-token' ,
132+ hasToken : true ,
133+ saveToken : mockSaveToken ,
134+ clearToken : mockClearToken ,
135+ isLoading : false ,
136+ error : null ,
137+ } ) ;
138+ window . history . replaceState ( { } , '' , 'http://localhost:3000/?url=https%3A%2F%2Fexample.com%2Farticles' ) ;
139+
140+ render ( < App /> ) ;
141+
142+ await waitFor ( ( ) => {
143+ expect ( mockConvertFeed ) . toHaveBeenCalledWith (
144+ 'https://example.com/articles' ,
145+ 'browserless' ,
146+ 'saved-token'
147+ ) ;
148+ } ) ;
149+ } ) ;
150+
105151 it ( 'shows inline token prompt when submitting without a token' , async ( ) => {
106152 render ( < App /> ) ;
107153
@@ -123,14 +169,55 @@ describe('App', () => {
123169 expect ( mockConvertFeed ) . not . toHaveBeenCalled ( ) ;
124170 } ) ;
125171
172+ it ( 'promotes included feeds when feed creation is disabled' , ( ) => {
173+ mockUseApiMetadata . mockReturnValue ( {
174+ metadata : {
175+ api : {
176+ name : 'html2rss-web API' ,
177+ description : 'RESTful API for converting websites to RSS feeds' ,
178+ openapi_url : 'http://example.test/openapi.yaml' ,
179+ } ,
180+ instance : {
181+ feed_creation : {
182+ enabled : false ,
183+ access_token_required : false ,
184+ } ,
185+ featured_feeds : [
186+ {
187+ path : '/microsoft.com/azure-products.rss' ,
188+ title : 'Azure product updates' ,
189+ description : 'Follow Microsoft Azure product announcements from your own instance.' ,
190+ } ,
191+ ] ,
192+ } ,
193+ } ,
194+ isLoading : false ,
195+ error : null ,
196+ } ) ;
197+
198+ render ( < App /> ) ;
199+
200+ expect ( screen . getByText ( 'Try a working included feed' ) ) . toBeInTheDocument ( ) ;
201+ expect ( screen . getByRole ( 'link' , { name : 'Azure product updates' } ) ) . toHaveAttribute (
202+ 'href' ,
203+ '/microsoft.com/azure-products.rss'
204+ ) ;
205+ expect ( screen . getByText ( 'Custom feed generation is disabled for this instance.' ) ) . toBeInTheDocument ( ) ;
206+ } ) ;
207+
126208 it ( 'renders the result panel when a feed is available' , async ( ) => {
209+ vi . spyOn ( window , 'fetch' ) . mockResolvedValue ( {
210+ ok : true ,
211+ json : async ( ) => ( { items : [ ] } ) ,
212+ } as Response ) ;
213+
127214 mockUseFeedConversion . mockReturnValue ( {
128215 isConverting : false ,
129216 result : {
130217 id : 'feed-123' ,
131218 name : 'Example Feed' ,
132219 url : 'https://example.com/articles' ,
133- strategy : 'ssrf_filter ' ,
220+ strategy : 'faraday ' ,
134221 feed_token : 'example-token' ,
135222 public_url : '/api/v1/feeds/example-token' ,
136223 json_public_url : '/api/v1/feeds/example-token.json' ,
@@ -196,7 +283,7 @@ describe('App', () => {
196283 expect ( mockSaveToken ) . toHaveBeenCalledWith ( 'token-123' ) ;
197284 expect ( mockConvertFeed ) . toHaveBeenCalledWith (
198285 'https://example.com/articles' ,
199- 'ssrf_filter ' ,
286+ 'browserless ' ,
200287 'token-123'
201288 ) ;
202289 } ) ;
@@ -281,4 +368,22 @@ describe('App', () => {
281368 expect ( bookmarklet . getAttribute ( 'href' ) ) . toContain ( '/?url=' ) ;
282369 expect ( bookmarklet . getAttribute ( 'href' ) ) . not . toContain ( '%27+encodeURIComponent' ) ;
283370 } ) ;
371+
372+ it ( 'shows the utility links in a user-focused order' , ( ) => {
373+ render ( < App /> ) ;
374+
375+ fireEvent . click ( screen . getByRole ( 'button' , { name : 'More' } ) ) ;
376+
377+ const utilityLinks = screen . getAllByRole ( 'link' ) . map ( ( link ) => link . textContent ) ;
378+ expect ( utilityLinks ) . toEqual ( [ 'Try included feeds' , 'Bookmarklet' , 'OpenAPI spec' , 'Source code' ] ) ;
379+
380+ expect ( screen . getByRole ( 'link' , { name : 'OpenAPI spec' } ) ) . toHaveAttribute (
381+ 'href' ,
382+ 'http://example.test/openapi.yaml'
383+ ) ;
384+ expect ( screen . getByRole ( 'link' , { name : 'Try included feeds' } ) ) . toHaveAttribute (
385+ 'href' ,
386+ 'https://html2rss.github.io/web-application/how-to/use-included-configs/'
387+ ) ;
388+ } ) ;
284389} ) ;
0 commit comments