diff --git a/src/pages/function-edit/FunctionEditPage.test.tsx b/src/pages/function-edit/FunctionEditPage.test.tsx
index 7a65709..44d4dea 100644
--- a/src/pages/function-edit/FunctionEditPage.test.tsx
+++ b/src/pages/function-edit/FunctionEditPage.test.tsx
@@ -16,9 +16,26 @@ let mockOnChange: ((value: string) => void) | undefined;
vi.mock('@openshift-console/dynamic-plugin-sdk', () => ({
DocumentTitle: ({ children }: { children: string }) => children,
ListPageHeader: ({ title }: { title: string }) => title,
- CodeEditor: ({ onChange }: { onChange?: (value: string) => void }) => {
+ CodeEditor: ({
+ onChange,
+ value,
+ language,
+ showEditor,
+ emptyState,
+ }: {
+ onChange?: (value: string) => void;
+ value?: string;
+ language?: string;
+ showEditor?: boolean;
+ emptyState?: unknown;
+ }) => {
mockOnChange = onChange;
- return 'CodeEditor';
+ if (!showEditor && emptyState) return emptyState;
+ return (
+
+ {value ?? ''}
+
+ );
},
}));
@@ -147,6 +164,230 @@ describe('FunctionEditPage', () => {
expect(screen.queryByText('Unsaved changes')).not.toBeInTheDocument();
});
+ it('shows selected file content in editor when tree item is clicked', async () => {
+ setupFetchHandlers();
+
+ renderEditPage('my-func');
+
+ await waitFor(() => {
+ expect(screen.getByText('func.yaml')).toBeInTheDocument();
+ });
+
+ await userEvent.setup().click(screen.getByText('func.yaml'));
+
+ await waitFor(() => {
+ expect(screen.getByTestId('code-editor')).toHaveTextContent('name: my-func');
+ });
+ });
+
+ it('marks hasChanges true after editing a file', async () => {
+ setupFetchHandlers();
+
+ renderEditPage('my-func');
+
+ await waitFor(() => {
+ expect(screen.getByText('index.js')).toBeInTheDocument();
+ });
+
+ expect(screen.getByRole('button', { name: /Save & Deploy/ })).toBeDisabled();
+
+ act(() => mockOnChange?.('const x = 1;'));
+
+ expect(screen.getByRole('button', { name: /Save & Deploy/ })).toBeEnabled();
+ });
+
+ it('resets hasChanges after save', async () => {
+ setupFetchHandlers();
+ setupPushHandlers();
+
+ renderEditPage('my-func');
+
+ await waitFor(() => {
+ expect(screen.getByText('index.js')).toBeInTheDocument();
+ });
+
+ act(() => mockOnChange?.('const x = 1;'));
+ expect(screen.getByRole('button', { name: /Save & Deploy/ })).toBeEnabled();
+
+ await userEvent.setup().click(screen.getByRole('button', { name: /Save & Deploy/ }));
+
+ await waitFor(() => {
+ expect(screen.getByRole('button', { name: /Save & Deploy/ })).toBeDisabled();
+ });
+ });
+
+ it('persists edited content when switching files and back', async () => {
+ setupFetchHandlers();
+
+ renderEditPage('my-func');
+
+ await waitFor(() => {
+ expect(screen.getByText('index.js')).toBeInTheDocument();
+ });
+
+ act(() => mockOnChange?.('edited module'));
+
+ const user = userEvent.setup();
+ await user.click(screen.getByText('func.yaml'));
+
+ await waitFor(() => {
+ expect(screen.getByTestId('code-editor')).toHaveTextContent('name: my-func');
+ });
+
+ // After editing, dirty indicator appends ● to the filename
+ await user.click(screen.getByText(/^index\.js/));
+
+ await waitFor(() => {
+ expect(screen.getByTestId('code-editor')).toHaveTextContent('edited module');
+ });
+ });
+
+ it('updates editor language when selecting a different file type', async () => {
+ setupFetchHandlers();
+
+ renderEditPage('my-func');
+
+ await waitFor(() => {
+ expect(screen.getByText('index.js')).toBeInTheDocument();
+ });
+
+ expect(screen.getByTestId('code-editor')).toHaveAttribute('data-language', 'javascript');
+
+ await userEvent.setup().click(screen.getByText('func.yaml'));
+
+ await waitFor(() => {
+ expect(screen.getByTestId('code-editor')).toHaveAttribute('data-language', 'yaml');
+ });
+ });
+
+ it('calls GitHub push API when saving edited files', async () => {
+ setupFetchHandlers();
+ setupPushHandlers();
+
+ const createTree = vi.fn();
+ server.use(
+ http.post(`${GITHUB_API}/repos/twoGiants/my-func/git/trees`, async ({ request }) => {
+ createTree(await request.json());
+ return HttpResponse.json({ sha: 'tree-sha' });
+ }),
+ );
+
+ renderEditPage('my-func');
+
+ await waitFor(() => {
+ expect(screen.getByText('index.js')).toBeInTheDocument();
+ });
+
+ act(() => mockOnChange?.('edited'));
+
+ await userEvent.setup().click(screen.getByRole('button', { name: /Save & Deploy/ }));
+
+ await waitFor(() => {
+ expect(createTree).toHaveBeenCalled();
+ });
+ });
+
+ it('shows danger alert when save fails', async () => {
+ setupFetchHandlers();
+ setupPushHandlers();
+ server.use(
+ http.post(`${GITHUB_API}/repos/twoGiants/my-func/git/blobs`, () =>
+ HttpResponse.json({ message: 'Server Error' }, { status: 500 }),
+ ),
+ );
+
+ renderEditPage('my-func');
+
+ await waitFor(() => {
+ expect(screen.getByText('index.js')).toBeInTheDocument();
+ });
+
+ act(() => mockOnChange?.('edited'));
+
+ await userEvent.setup().click(screen.getByRole('button', { name: /Save & Deploy/ }));
+
+ await waitFor(() => {
+ expect(screen.getByText('Server Error')).toBeInTheDocument();
+ });
+ });
+
+ it('disables save button while saving is in progress', async () => {
+ setupFetchHandlers();
+ setupPushHandlers();
+ server.use(
+ http.post(`${GITHUB_API}/repos/twoGiants/my-func/git/blobs`, async () => {
+ await delay('infinite');
+ return HttpResponse.json({ sha: 'blob-sha' });
+ }),
+ );
+
+ renderEditPage('my-func');
+
+ await waitFor(() => {
+ expect(screen.getByText('index.js')).toBeInTheDocument();
+ });
+
+ act(() => mockOnChange?.('edited'));
+
+ await userEvent.setup().click(screen.getByRole('button', { name: /Save & Deploy/ }));
+
+ await waitFor(() => {
+ expect(screen.getByRole('button', { name: /Save & Deploy/ })).toBeDisabled();
+ });
+ });
+
+ it('clears error alert when next save succeeds', async () => {
+ setupFetchHandlers();
+ setupPushHandlers();
+
+ server.use(
+ http.post(`${GITHUB_API}/repos/twoGiants/my-func/git/blobs`, () =>
+ HttpResponse.json({ message: 'Server Error' }, { status: 500 }),
+ ),
+ );
+
+ renderEditPage('my-func');
+
+ await waitFor(() => {
+ expect(screen.getByText('index.js')).toBeInTheDocument();
+ });
+
+ act(() => mockOnChange?.('edited'));
+
+ const user = userEvent.setup();
+ await user.click(screen.getByRole('button', { name: /Save & Deploy/ }));
+
+ await waitFor(() => {
+ expect(screen.getByText('Server Error')).toBeInTheDocument();
+ });
+
+ server.use(
+ http.post(`${GITHUB_API}/repos/twoGiants/my-func/git/blobs`, () =>
+ HttpResponse.json({ sha: 'blob-sha' }),
+ ),
+ );
+
+ act(() => mockOnChange?.('edited again'));
+ await user.click(screen.getByRole('button', { name: /Save & Deploy/ }));
+
+ await waitFor(() => {
+ expect(screen.getByText('Pushed to GitHub. Deployment running...')).toBeInTheDocument();
+ });
+ });
+
+ it('shows empty state placeholder when no file is selected', async () => {
+ renderEditPage('nonexistent');
+
+ await waitFor(() => {
+ expect(screen.getByText('No files')).toBeInTheDocument();
+ });
+
+ expect(screen.getByText('Start editing')).toBeInTheDocument();
+ expect(
+ screen.getByText('Select a file from the tree view to start editing.'),
+ ).toBeInTheDocument();
+ });
+
it('shows success message after save and hides it after 2 seconds', async () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
setupFetchHandlers();
@@ -182,7 +423,7 @@ function setupPushHandlers() {
http.get(`${GITHUB_API}/repos/twoGiants/my-func/git/ref/:ref+`, () =>
HttpResponse.json({ object: { sha: 'head-sha' } }),
),
- http.get(`${GITHUB_API}/repos/twoGiants/my-func/git/commits/head-sha`, () =>
+ http.get(`${GITHUB_API}/repos/twoGiants/my-func/git/commits/:sha`, () =>
HttpResponse.json({ tree: { sha: 'parent-tree-sha' } }),
),
http.post(`${GITHUB_API}/repos/twoGiants/my-func/git/blobs`, () =>