@@ -16,9 +16,26 @@ let mockOnChange: ((value: string) => void) | undefined;
1616vi . mock ( '@openshift-console/dynamic-plugin-sdk' , ( ) => ( {
1717 DocumentTitle : ( { children } : { children : string } ) => children ,
1818 ListPageHeader : ( { title } : { title : string } ) => title ,
19- CodeEditor : ( { onChange } : { onChange ?: ( value : string ) => void } ) => {
19+ CodeEditor : ( {
20+ onChange,
21+ value,
22+ language,
23+ showEditor,
24+ emptyState,
25+ } : {
26+ onChange ?: ( value : string ) => void ;
27+ value ?: string ;
28+ language ?: string ;
29+ showEditor ?: boolean ;
30+ emptyState ?: unknown ;
31+ } ) => {
2032 mockOnChange = onChange ;
21- return 'CodeEditor' ;
33+ if ( ! showEditor && emptyState ) return emptyState ;
34+ return (
35+ < div data-testid = "code-editor" data-language = { language ?? '' } >
36+ { value ?? '' }
37+ </ div >
38+ ) ;
2239 } ,
2340} ) ) ;
2441
@@ -147,6 +164,230 @@ describe('FunctionEditPage', () => {
147164 expect ( screen . queryByText ( 'Unsaved changes' ) ) . not . toBeInTheDocument ( ) ;
148165 } ) ;
149166
167+ it ( 'shows selected file content in editor when tree item is clicked' , async ( ) => {
168+ setupFetchHandlers ( ) ;
169+
170+ renderEditPage ( 'my-func' ) ;
171+
172+ await waitFor ( ( ) => {
173+ expect ( screen . getByText ( 'func.yaml' ) ) . toBeInTheDocument ( ) ;
174+ } ) ;
175+
176+ await userEvent . setup ( ) . click ( screen . getByText ( 'func.yaml' ) ) ;
177+
178+ await waitFor ( ( ) => {
179+ expect ( screen . getByTestId ( 'code-editor' ) ) . toHaveTextContent ( 'name: my-func' ) ;
180+ } ) ;
181+ } ) ;
182+
183+ it ( 'marks hasChanges true after editing a file' , async ( ) => {
184+ setupFetchHandlers ( ) ;
185+
186+ renderEditPage ( 'my-func' ) ;
187+
188+ await waitFor ( ( ) => {
189+ expect ( screen . getByText ( 'index.js' ) ) . toBeInTheDocument ( ) ;
190+ } ) ;
191+
192+ expect ( screen . getByRole ( 'button' , { name : / S a v e & D e p l o y / } ) ) . toBeDisabled ( ) ;
193+
194+ act ( ( ) => mockOnChange ?.( 'const x = 1;' ) ) ;
195+
196+ expect ( screen . getByRole ( 'button' , { name : / S a v e & D e p l o y / } ) ) . toBeEnabled ( ) ;
197+ } ) ;
198+
199+ it ( 'resets hasChanges after save' , async ( ) => {
200+ setupFetchHandlers ( ) ;
201+ setupPushHandlers ( ) ;
202+
203+ renderEditPage ( 'my-func' ) ;
204+
205+ await waitFor ( ( ) => {
206+ expect ( screen . getByText ( 'index.js' ) ) . toBeInTheDocument ( ) ;
207+ } ) ;
208+
209+ act ( ( ) => mockOnChange ?.( 'const x = 1;' ) ) ;
210+ expect ( screen . getByRole ( 'button' , { name : / S a v e & D e p l o y / } ) ) . toBeEnabled ( ) ;
211+
212+ await userEvent . setup ( ) . click ( screen . getByRole ( 'button' , { name : / S a v e & D e p l o y / } ) ) ;
213+
214+ await waitFor ( ( ) => {
215+ expect ( screen . getByRole ( 'button' , { name : / S a v e & D e p l o y / } ) ) . toBeDisabled ( ) ;
216+ } ) ;
217+ } ) ;
218+
219+ it ( 'persists edited content when switching files and back' , async ( ) => {
220+ setupFetchHandlers ( ) ;
221+
222+ renderEditPage ( 'my-func' ) ;
223+
224+ await waitFor ( ( ) => {
225+ expect ( screen . getByText ( 'index.js' ) ) . toBeInTheDocument ( ) ;
226+ } ) ;
227+
228+ act ( ( ) => mockOnChange ?.( 'edited module' ) ) ;
229+
230+ const user = userEvent . setup ( ) ;
231+ await user . click ( screen . getByText ( 'func.yaml' ) ) ;
232+
233+ await waitFor ( ( ) => {
234+ expect ( screen . getByTestId ( 'code-editor' ) ) . toHaveTextContent ( 'name: my-func' ) ;
235+ } ) ;
236+
237+ // After editing, dirty indicator appends ● to the filename
238+ await user . click ( screen . getByText ( / ^ i n d e x \. j s / ) ) ;
239+
240+ await waitFor ( ( ) => {
241+ expect ( screen . getByTestId ( 'code-editor' ) ) . toHaveTextContent ( 'edited module' ) ;
242+ } ) ;
243+ } ) ;
244+
245+ it ( 'updates editor language when selecting a different file type' , async ( ) => {
246+ setupFetchHandlers ( ) ;
247+
248+ renderEditPage ( 'my-func' ) ;
249+
250+ await waitFor ( ( ) => {
251+ expect ( screen . getByText ( 'index.js' ) ) . toBeInTheDocument ( ) ;
252+ } ) ;
253+
254+ expect ( screen . getByTestId ( 'code-editor' ) ) . toHaveAttribute ( 'data-language' , 'javascript' ) ;
255+
256+ await userEvent . setup ( ) . click ( screen . getByText ( 'func.yaml' ) ) ;
257+
258+ await waitFor ( ( ) => {
259+ expect ( screen . getByTestId ( 'code-editor' ) ) . toHaveAttribute ( 'data-language' , 'yaml' ) ;
260+ } ) ;
261+ } ) ;
262+
263+ it ( 'calls GitHub push API when saving edited files' , async ( ) => {
264+ setupFetchHandlers ( ) ;
265+ setupPushHandlers ( ) ;
266+
267+ const createTree = vi . fn ( ) ;
268+ server . use (
269+ http . post ( `${ GITHUB_API } /repos/twoGiants/my-func/git/trees` , async ( { request } ) => {
270+ createTree ( await request . json ( ) ) ;
271+ return HttpResponse . json ( { sha : 'tree-sha' } ) ;
272+ } ) ,
273+ ) ;
274+
275+ renderEditPage ( 'my-func' ) ;
276+
277+ await waitFor ( ( ) => {
278+ expect ( screen . getByText ( 'index.js' ) ) . toBeInTheDocument ( ) ;
279+ } ) ;
280+
281+ act ( ( ) => mockOnChange ?.( 'edited' ) ) ;
282+
283+ await userEvent . setup ( ) . click ( screen . getByRole ( 'button' , { name : / S a v e & D e p l o y / } ) ) ;
284+
285+ await waitFor ( ( ) => {
286+ expect ( createTree ) . toHaveBeenCalled ( ) ;
287+ } ) ;
288+ } ) ;
289+
290+ it ( 'shows danger alert when save fails' , async ( ) => {
291+ setupFetchHandlers ( ) ;
292+ setupPushHandlers ( ) ;
293+ server . use (
294+ http . post ( `${ GITHUB_API } /repos/twoGiants/my-func/git/blobs` , ( ) =>
295+ HttpResponse . json ( { message : 'Server Error' } , { status : 500 } ) ,
296+ ) ,
297+ ) ;
298+
299+ renderEditPage ( 'my-func' ) ;
300+
301+ await waitFor ( ( ) => {
302+ expect ( screen . getByText ( 'index.js' ) ) . toBeInTheDocument ( ) ;
303+ } ) ;
304+
305+ act ( ( ) => mockOnChange ?.( 'edited' ) ) ;
306+
307+ await userEvent . setup ( ) . click ( screen . getByRole ( 'button' , { name : / S a v e & D e p l o y / } ) ) ;
308+
309+ await waitFor ( ( ) => {
310+ expect ( screen . getByText ( 'Server Error' ) ) . toBeInTheDocument ( ) ;
311+ } ) ;
312+ } ) ;
313+
314+ it ( 'disables save button while saving is in progress' , async ( ) => {
315+ setupFetchHandlers ( ) ;
316+ setupPushHandlers ( ) ;
317+ server . use (
318+ http . post ( `${ GITHUB_API } /repos/twoGiants/my-func/git/blobs` , async ( ) => {
319+ await delay ( 'infinite' ) ;
320+ return HttpResponse . json ( { sha : 'blob-sha' } ) ;
321+ } ) ,
322+ ) ;
323+
324+ renderEditPage ( 'my-func' ) ;
325+
326+ await waitFor ( ( ) => {
327+ expect ( screen . getByText ( 'index.js' ) ) . toBeInTheDocument ( ) ;
328+ } ) ;
329+
330+ act ( ( ) => mockOnChange ?.( 'edited' ) ) ;
331+
332+ await userEvent . setup ( ) . click ( screen . getByRole ( 'button' , { name : / S a v e & D e p l o y / } ) ) ;
333+
334+ await waitFor ( ( ) => {
335+ expect ( screen . getByRole ( 'button' , { name : / S a v e & D e p l o y / } ) ) . toBeDisabled ( ) ;
336+ } ) ;
337+ } ) ;
338+
339+ it ( 'clears error alert when next save succeeds' , async ( ) => {
340+ setupFetchHandlers ( ) ;
341+ setupPushHandlers ( ) ;
342+
343+ server . use (
344+ http . post ( `${ GITHUB_API } /repos/twoGiants/my-func/git/blobs` , ( ) =>
345+ HttpResponse . json ( { message : 'Server Error' } , { status : 500 } ) ,
346+ ) ,
347+ ) ;
348+
349+ renderEditPage ( 'my-func' ) ;
350+
351+ await waitFor ( ( ) => {
352+ expect ( screen . getByText ( 'index.js' ) ) . toBeInTheDocument ( ) ;
353+ } ) ;
354+
355+ act ( ( ) => mockOnChange ?.( 'edited' ) ) ;
356+
357+ const user = userEvent . setup ( ) ;
358+ await user . click ( screen . getByRole ( 'button' , { name : / S a v e & D e p l o y / } ) ) ;
359+
360+ await waitFor ( ( ) => {
361+ expect ( screen . getByText ( 'Server Error' ) ) . toBeInTheDocument ( ) ;
362+ } ) ;
363+
364+ server . use (
365+ http . post ( `${ GITHUB_API } /repos/twoGiants/my-func/git/blobs` , ( ) =>
366+ HttpResponse . json ( { sha : 'blob-sha' } ) ,
367+ ) ,
368+ ) ;
369+
370+ act ( ( ) => mockOnChange ?.( 'edited again' ) ) ;
371+ await user . click ( screen . getByRole ( 'button' , { name : / S a v e & D e p l o y / } ) ) ;
372+
373+ await waitFor ( ( ) => {
374+ expect ( screen . getByText ( 'Pushed to GitHub. Deployment running...' ) ) . toBeInTheDocument ( ) ;
375+ } ) ;
376+ } ) ;
377+
378+ it ( 'shows empty state placeholder when no file is selected' , async ( ) => {
379+ renderEditPage ( 'nonexistent' ) ;
380+
381+ await waitFor ( ( ) => {
382+ expect ( screen . getByText ( 'No files' ) ) . toBeInTheDocument ( ) ;
383+ } ) ;
384+
385+ expect ( screen . getByText ( 'Start editing' ) ) . toBeInTheDocument ( ) ;
386+ expect (
387+ screen . getByText ( 'Select a file from the tree view to start editing.' ) ,
388+ ) . toBeInTheDocument ( ) ;
389+ } ) ;
390+
150391 it ( 'shows success message after save and hides it after 2 seconds' , async ( ) => {
151392 vi . useFakeTimers ( { shouldAdvanceTime : true } ) ;
152393 setupFetchHandlers ( ) ;
@@ -182,7 +423,7 @@ function setupPushHandlers() {
182423 http . get ( `${ GITHUB_API } /repos/twoGiants/my-func/git/ref/:ref+` , ( ) =>
183424 HttpResponse . json ( { object : { sha : 'head-sha' } } ) ,
184425 ) ,
185- http . get ( `${ GITHUB_API } /repos/twoGiants/my-func/git/commits/head- sha` , ( ) =>
426+ http . get ( `${ GITHUB_API } /repos/twoGiants/my-func/git/commits/: sha` , ( ) =>
186427 HttpResponse . json ( { tree : { sha : 'parent-tree-sha' } } ) ,
187428 ) ,
188429 http . post ( `${ GITHUB_API } /repos/twoGiants/my-func/git/blobs` , ( ) =>
0 commit comments