@@ -70,6 +70,148 @@ const runTests = () => {
7070 } ) ;
7171} ;
7272
73+ describe ( "Newsfeed module > Notifications" , ( ) => {
74+ let page ;
75+
76+ afterAll ( async ( ) => {
77+ await helpers . stopApplication ( ) ;
78+ } ) ;
79+
80+ /**
81+ * Helper: call notificationReceived on the newsfeed module directly.
82+ * @param {object } p - playwright page
83+ * @param {string } notification - notification name
84+ * @param {object } payload - notification payload
85+ * @returns {Promise<void> } resolves when the notification has been dispatched
86+ */
87+ const notify = ( p , notification , payload = { } ) => p . evaluate (
88+ ( { n, pl } ) => {
89+ const nf = MM . getModules ( ) . find ( ( m ) => m . name === "newsfeed" ) ;
90+ nf . notificationReceived ( n , pl , nf ) ;
91+ } ,
92+ { n : notification , pl : payload }
93+ ) ;
94+
95+ beforeAll ( async ( ) => {
96+ await helpers . startApplication ( "tests/configs/modules/newsfeed/notifications.js" ) ;
97+ await helpers . getDocument ( ) ;
98+ page = helpers . getPage ( ) ;
99+ await expect ( page . locator ( ".newsfeed .newsfeed-title" ) ) . toBeVisible ( ) ;
100+ } ) ;
101+
102+ it ( "ARTICLE_NEXT should show the next article" , async ( ) => {
103+ const title1 = await page . locator ( ".newsfeed .newsfeed-title" ) . textContent ( ) ;
104+ await notify ( page , "ARTICLE_NEXT" ) ;
105+ await expect ( page . locator ( ".newsfeed .newsfeed-title" ) ) . not . toContainText ( title1 . trim ( ) ) ;
106+ } ) ;
107+
108+ it ( "ARTICLE_PREVIOUS should return to the previous article" , async ( ) => {
109+ // Start at article 0, go to article 1, then back
110+ await page . evaluate ( ( ) => {
111+ const nf = MM . getModules ( ) . find ( ( m ) => m . name === "newsfeed" ) ;
112+ nf . activeItem = 0 ;
113+ nf . resetDescrOrFullArticleAndTimer ( ) ;
114+ nf . updateDom ( 0 ) ;
115+ } ) ;
116+ await expect ( page . locator ( ".newsfeed .newsfeed-title" ) ) . toContainText ( "QPanel" ) ;
117+ const title0 = await page . locator ( ".newsfeed .newsfeed-title" ) . textContent ( ) ;
118+
119+ await notify ( page , "ARTICLE_NEXT" ) ;
120+ await expect ( page . locator ( ".newsfeed .newsfeed-title" ) ) . not . toContainText ( title0 . trim ( ) ) ;
121+
122+ await notify ( page , "ARTICLE_PREVIOUS" ) ;
123+ await expect ( page . locator ( ".newsfeed .newsfeed-title" ) ) . toContainText ( title0 . trim ( ) ) ;
124+ } ) ;
125+
126+ it ( "ARTICLE_NEXT should wrap around from the last article to the first" , async ( ) => {
127+ // Jump to the last article
128+ await page . evaluate ( ( ) => {
129+ const nf = MM . getModules ( ) . find ( ( m ) => m . name === "newsfeed" ) ;
130+ nf . activeItem = nf . newsItems . length - 1 ;
131+ nf . resetDescrOrFullArticleAndTimer ( ) ;
132+ nf . updateDom ( 0 ) ;
133+ } ) ;
134+ await expect ( page . locator ( ".newsfeed .newsfeed-title" ) ) . toBeVisible ( ) ;
135+ const titleLast = await page . locator ( ".newsfeed .newsfeed-title" ) . textContent ( ) ;
136+
137+ await notify ( page , "ARTICLE_NEXT" ) ;
138+ await expect ( page . locator ( ".newsfeed .newsfeed-title" ) ) . not . toContainText ( titleLast . trim ( ) ) ;
139+
140+ // activeItem should now be 0
141+ const activeItem = await page . evaluate ( ( ) => MM . getModules ( ) . find ( ( m ) => m . name === "newsfeed" ) . activeItem ) ;
142+ expect ( activeItem ) . toBe ( 0 ) ;
143+ } ) ;
144+
145+ it ( "ARTICLE_PREVIOUS should wrap around from the first article to the last" , async ( ) => {
146+ await page . evaluate ( ( ) => {
147+ const nf = MM . getModules ( ) . find ( ( m ) => m . name === "newsfeed" ) ;
148+ nf . activeItem = 0 ;
149+ nf . resetDescrOrFullArticleAndTimer ( ) ;
150+ } ) ;
151+ await notify ( page , "ARTICLE_PREVIOUS" ) ;
152+
153+ const activeItem = await page . evaluate ( ( ) => {
154+ const nf = MM . getModules ( ) . find ( ( m ) => m . name === "newsfeed" ) ;
155+ return { activeItem : nf . activeItem , total : nf . newsItems . length } ;
156+ } ) ;
157+ expect ( activeItem . activeItem ) . toBe ( activeItem . total - 1 ) ;
158+ } ) ;
159+
160+ it ( "ARTICLE_INFO_REQUEST should respond with title, source, date, desc and raw url" , async ( ) => {
161+ await page . evaluate ( ( ) => {
162+ const nf = MM . getModules ( ) . find ( ( m ) => m . name === "newsfeed" ) ;
163+ nf . activeItem = 0 ;
164+ nf . resetDescrOrFullArticleAndTimer ( ) ;
165+ } ) ;
166+
167+ const info = await page . evaluate ( ( ) => new Promise ( ( resolve , reject ) => {
168+ const timer = setTimeout ( ( ) => reject ( new Error ( "ARTICLE_INFO_RESPONSE timeout" ) ) , 3000 ) ;
169+ const origSend = MM . sendNotification . bind ( MM ) ;
170+ MM . sendNotification = function ( n , p , s ) {
171+ if ( n === "ARTICLE_INFO_RESPONSE" ) {
172+ clearTimeout ( timer ) ;
173+ MM . sendNotification = origSend ;
174+ resolve ( p ) ;
175+ }
176+ return origSend ( n , p , s ) ;
177+ } ;
178+ const nf = MM . getModules ( ) . find ( ( m ) => m . name === "newsfeed" ) ;
179+ nf . notificationReceived ( "ARTICLE_INFO_REQUEST" , { } , nf ) ;
180+ } ) ) ;
181+
182+ expect ( info ) . toHaveProperty ( "title" ) ;
183+ expect ( info ) . toHaveProperty ( "source" ) ;
184+ expect ( info ) . toHaveProperty ( "date" ) ;
185+ expect ( info ) . toHaveProperty ( "desc" ) ;
186+ expect ( info ) . toHaveProperty ( "url" ) ;
187+ expect ( info . title ) . toBe ( "QPanel 0.13.0" ) ;
188+ expect ( info . source ) . toBe ( "Rodrigo Ramirez Blog" ) ;
189+ // URL must be the raw article URL, not a CORS proxy URL
190+ expect ( info . url ) . toMatch ( / ^ h t t p s ? : \/ \/ / ) ;
191+ expect ( info . url ) . not . toContain ( "localhost" ) ;
192+ } ) ;
193+
194+ it ( "ARTICLE_LESS_DETAILS should reset the full article view" , async ( ) => {
195+ // Simulate full article view being active
196+ await page . evaluate ( ( ) => {
197+ const nf = MM . getModules ( ) . find ( ( m ) => m . name === "newsfeed" ) ;
198+ nf . config . showFullArticle = true ;
199+ nf . articleFrameCheckPending = false ;
200+ nf . articleUnavailable = false ;
201+ } ) ;
202+
203+ await notify ( page , "ARTICLE_LESS_DETAILS" ) ;
204+
205+ const state = await page . evaluate ( ( ) => {
206+ const nf = MM . getModules ( ) . find ( ( m ) => m . name === "newsfeed" ) ;
207+ return { showFullArticle : nf . config . showFullArticle } ;
208+ } ) ;
209+ expect ( state . showFullArticle ) . toBe ( false ) ;
210+ // Normal newsfeed title should be visible again
211+ await expect ( page . locator ( ".newsfeed .newsfeed-title" ) ) . toBeVisible ( ) ;
212+ } ) ;
213+ } ) ;
214+
73215describe ( "Newsfeed module" , ( ) => {
74216 afterAll ( async ( ) => {
75217 await helpers . stopApplication ( ) ;
0 commit comments