diff --git a/webpack/assets/javascripts/react_app/components/Editor/Editor.js b/webpack/assets/javascripts/react_app/components/Editor/Editor.js
index 62268efa96..a183d65289 100644
--- a/webpack/assets/javascripts/react_app/components/Editor/Editor.js
+++ b/webpack/assets/javascripts/react_app/components/Editor/Editor.js
@@ -1,8 +1,10 @@
-import React from 'react';
+/* eslint-disable max-lines */
+import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
-import { Alert, AlertActionCloseButton } from '@patternfly/react-core';
+import { Alert, AlertActionCloseButton, Button } from '@patternfly/react-core';
import { noop } from '../../common/helpers';
+import { translate as __ } from '../../common/I18n';
import DiffView from '../DiffView/DiffView';
import EditorView from './components/EditorView';
import EditorNavbar from './components/EditorNavbar';
@@ -14,20 +16,65 @@ import {
} from './EditorConstants';
import './editor.scss';
-class Editor extends React.Component {
- componentDidMount() {
- const {
- data: { hosts, templateClass, locked, template, type, dslCache },
- initializeEditor,
- isMasked,
- isRendering,
- readOnly,
- previewResult,
- selectedView,
- showError,
- } = this.props;
+const Editor = ({
+ data: {
+ name,
+ hosts,
+ isSafemodeEnabled,
+ locked,
+ renderPath,
+ safemodeRenderPath,
+ showImport,
+ showPreview,
+ showHostSelector,
+ template,
+ templateClass,
+ title,
+ type,
+ dslCache,
+ },
+ changeDiffViewType,
+ changeEditorValue,
+ changeSetting,
+ changeTab,
+ diffViewType,
+ dismissErrorToast,
+ editorName,
+ errorText,
+ fetchAndPreview,
+ filteredHosts,
+ hosts: componentHosts,
+ importFile,
+ initializeEditor,
+ isFetchingHosts,
+ isLoading,
+ isMasked,
+ isMaximized,
+ isRendering,
+ isSearchingHosts,
+ keyBinding,
+ mode,
+ onHostSearch,
+ previewResult,
+ previewTemplate,
+ readOnly,
+ renderedEditorValue,
+ revertChanges,
+ selectedHost,
+ selectedView,
+ showError,
+ theme,
+ autocompletion,
+ liveAutocompletion,
+ toggleModal,
+ toggleRenderView,
+ value,
+ templateKindId,
+}) => {
+ const [safemode, setSafemode] = useState(isSafemodeEnabled);
- const initializeData = {
+ useEffect(() => {
+ initializeEditor({
hosts,
isMasked,
templateClass,
@@ -40,195 +87,170 @@ class Editor extends React.Component {
template,
type,
dslCache,
- };
- initializeEditor(initializeData);
- }
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
- render() {
- const {
- data: {
- name,
- isSafemodeEnabled,
- renderPath,
- safemodeRenderPath,
- showImport,
- showPreview,
- showHostSelector,
- template,
- title,
- },
- changeDiffViewType,
- changeEditorValue,
- changeSetting,
- changeTab,
- diffViewType,
- dismissErrorToast,
- editorName,
- errorText,
- fetchAndPreview,
- filteredHosts,
- hosts,
- importFile,
- isFetchingHosts,
- isLoading,
- isMasked,
- isMaximized,
- isRendering,
- isSearchingHosts,
- isSelectOpen,
- keyBinding,
- mode,
- onHostSearch,
- onHostSelectToggle,
- onSearchClear,
- previewResult,
- previewTemplate,
- readOnly,
- renderedEditorValue,
- revertChanges,
- searchQuery,
- selectedHost,
- selectedView,
- showError,
- theme,
- autocompletion,
- liveAutocompletion,
- toggleModal,
- toggleRenderView,
- value,
+ const handleSafeModeChange = ({ currentTarget: { checked } }) => {
+ const newRenderPath = checked ? safemodeRenderPath : renderPath;
+
+ setSafemode(checked);
+ previewTemplate({
+ host: selectedHost,
+ renderPath: newRenderPath,
templateKindId,
- } = this.props;
+ });
+ };
- const editorViewProps = {
- value: isRendering ? previewResult : value,
- mode: isRendering ? 'Text' : mode,
- theme,
- autocompletion,
- liveAutocompletion,
- keyBinding,
- onChange: isRendering ? noop : changeEditorValue,
- readOnly: readOnly || isRendering,
- isMasked,
- };
- const editorNameTab = {
- input: `${editorName}Code`,
- preview: `${editorName}Preview`,
- };
+ const editorViewProps = {
+ value: isRendering ? previewResult : value,
+ mode: isRendering ? 'Text' : mode,
+ theme,
+ autocompletion,
+ liveAutocompletion,
+ keyBinding,
+ onChange: isRendering ? noop : changeEditorValue,
+ readOnly: readOnly || isRendering,
+ isMasked,
+ };
+ const editorNameTab = {
+ input: `${editorName}Code`,
+ preview: `${editorName}Preview`,
+ };
+ const selectedRenderPath = safemode ? safemodeRenderPath : renderPath;
- return (
-
-
dismissErrorToast()}
- />
- }
- title={errorText}
- />
-
-
-
-
-
+ dismissErrorToast()}
+ />
+ }
+ title={errorText}
+ />
+ {selectedView === 'preview' &&
+ previewResult !== '' &&
+ renderedEditorValue !== value && (
+
+ {__('Preview is outdated.')}
+
+ >
+ }
/>
-
-
- {!readOnly && (
-
)}
+
+
+
+
+
- );
- }
-}
+
+ {!readOnly && (
+
+ )}
+
+ );
+};
Editor.propTypes = {
data: PropTypes.shape({
@@ -284,11 +306,7 @@ Editor.propTypes = {
value: PropTypes.string.isRequired,
templateKindId: PropTypes.string,
renderedEditorValue: PropTypes.string.isRequired,
- isSelectOpen: PropTypes.bool.isRequired,
- searchQuery: PropTypes.string.isRequired,
- onHostSelectToggle: PropTypes.func.isRequired,
onHostSearch: PropTypes.func.isRequired,
- onSearchClear: PropTypes.func.isRequired,
isSearchingHosts: PropTypes.bool.isRequired,
fetchAndPreview: PropTypes.func.isRequired,
};
diff --git a/webpack/assets/javascripts/react_app/components/Editor/__tests__/Editor.test.js b/webpack/assets/javascripts/react_app/components/Editor/__tests__/Editor.test.js
index 2a741625fc..08cd490388 100644
--- a/webpack/assets/javascripts/react_app/components/Editor/__tests__/Editor.test.js
+++ b/webpack/assets/javascripts/react_app/components/Editor/__tests__/Editor.test.js
@@ -1,107 +1,172 @@
import React from 'react';
-import { act } from '@testing-library/react';
-import { mount } from 'enzyme';
-import { testComponentSnapshotsWithFixtures } from '../../../common/testHelpers';
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+import { rtlHelpers } from '../../../common/rtlTestHelpers';
+
import Editor from '../Editor';
import { editorOptions } from '../Editor.fixtures';
-const didMountStubs = () => ({
- changeState: jest.fn(),
- importFile: jest.fn(),
- revertChanges: jest.fn(),
- previewTemplate: jest.fn(),
- initializeEditor: jest.fn(),
-});
+const { renderWithI18n } = rtlHelpers;
+
+const renderEditor = (props = {}) => {
+ const initializeEditor = jest.fn();
-const fixtures = {
- 'renders editor': editorOptions,
+ return {
+ initializeEditor,
+ ...renderWithI18n(
+
+ ),
+ };
};
describe('Editor', () => {
- jest.useFakeTimers();
- describe('rendering', () =>
- testComponentSnapshotsWithFixtures(Editor, fixtures));
-
- describe('triggering', () => {
- it('should trigger input view', async () => {
- const props = { ...editorOptions, ...didMountStubs() };
- const component = mount();
- await act(async () => jest.advanceTimersByTime(1000));
- expect(
- component
- .find('li[role="presentation"]')
- .at(0)
- .hasClass('active')
- ).toBe(true);
+ it('renders editor tabs', async () => {
+ renderEditor();
+
+ expect(await screen.findByRole('tab', { name: 'Editor' })).toBeInTheDocument();
+ expect(screen.getByRole('tab', { name: 'Changes' })).toBeInTheDocument();
+ expect(screen.getByRole('tab', { name: 'Preview' })).toBeInTheDocument();
+ });
+
+ it('calls initializeEditor on mount', async () => {
+ const { initializeEditor } = renderEditor();
+
+ await screen.findByRole('tab', { name: 'Editor' });
+
+ expect(initializeEditor).toHaveBeenCalledTimes(1);
+ expect(initializeEditor).toHaveBeenCalledWith(
+ expect.objectContaining({
+ selectedView: editorOptions.selectedView,
+ template: editorOptions.data.template,
+ })
+ );
+ });
+
+ it('selects the input tab by default', async () => {
+ renderEditor({ selectedView: 'input' });
+
+ expect(await screen.findByRole('tab', { name: 'Editor' })).toHaveAttribute(
+ 'aria-selected',
+ 'true'
+ );
+ });
+
+ it('selects the diff tab when diff view is active', async () => {
+ renderEditor({ selectedView: 'diff' });
+
+ expect(await screen.findByRole('tab', { name: 'Changes' })).toHaveAttribute(
+ 'aria-selected',
+ 'true'
+ );
+ });
+
+ it('selects the preview tab when preview view is active', async () => {
+ renderEditor({ selectedView: 'preview', isRendering: true });
+
+ expect(await screen.findByRole('tab', { name: 'Preview' })).toHaveAttribute(
+ 'aria-selected',
+ 'true'
+ );
+ });
+
+ it('shows the diff table when diff view is active', async () => {
+ renderEditor({
+ selectedView: 'diff',
+ data: { ...editorOptions.data, template: 'old template' },
+ value: 'new value',
});
- it('should trigger input view with no template', async () => {
- const props = {
- ...editorOptions,
- ...didMountStubs(),
- data: { ...editorOptions.data, template: null },
- };
- const component = mount();
- await act(async () => jest.advanceTimersByTime(1000));
- expect(component.props().template).toBe(' />');
+
+ expect(await screen.findByRole('tab', { name: 'Changes' })).toHaveAttribute(
+ 'aria-selected',
+ 'true'
+ );
+ expect(screen.getByText('old template')).toBeInTheDocument();
+ expect(screen.getAllByText('new value').length).toBeGreaterThan(0);
+ });
+
+ it('shows outdated preview warning', async () => {
+ renderEditor({
+ selectedView: 'preview',
+ previewResult: 'rendered preview',
+ renderedEditorValue: 'old rendered value',
+ value: 'current value',
});
- it('should trigger diff view', async () => {
- const props = {
- ...editorOptions,
- ...didMountStubs(),
- selectedView: 'diff',
- };
- const component = mount();
- await act(async () => jest.advanceTimersByTime(1000));
- expect(
- component
- .find('li[role="presentation"]')
- .at(1)
- .hasClass('active')
- ).toBe(true);
+
+ expect(await screen.findByText('Preview is outdated.')).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'Preview' })
+ ).toBeInTheDocument();
+ });
+
+ it('refreshes preview when outdated preview link is clicked', async () => {
+ const previewTemplate = jest.fn();
+
+ renderEditor({
+ previewTemplate,
+ selectedView: 'preview',
+ previewResult: 'rendered preview',
+ renderedEditorValue: 'old rendered value',
+ value: 'current value',
+ selectedHost: { id: '1', name: 'host1' },
+ data: {
+ ...editorOptions.data,
+ safemodeRenderPath: '/safemode/path',
+ },
});
- it('should trigger preview view', async () => {
- const props = {
- ...editorOptions,
- ...didMountStubs(),
- selectedView: 'preview',
- isRendering: true,
- };
- const wrapper = mount();
- wrapper.find('button.close').simulate('click');
- await act(async () => jest.advanceTimersByTime(1000));
- const component = mount();
- await act(async () => jest.advanceTimersByTime(1000));
-
- expect(
- component
- .find('li[role="presentation"]')
- .at(2)
- .hasClass('active')
- ).toBe(true);
+
+ userEvent.click(
+ await screen.findByRole('button', { name: 'Preview' })
+ );
+
+ expect(previewTemplate).toHaveBeenCalledWith({
+ host: { id: '1', name: 'host1' },
+ renderPath: '/safemode/path',
+ templateKindId: '',
});
});
- it('should trigger hidden value editor', async () => {
- const props = {
- ...editorOptions,
- ...didMountStubs(),
- selectedView: 'preview',
- isRendering: true,
- isMasked: true,
- };
- const wrapper = mount();
- await act(async () => jest.advanceTimersByTime(1000));
- expect(wrapper.find('.mask-editor').exists()).toBe(true);
+
+ it('dismisses the preview error toast', async () => {
+ const dismissErrorToast = jest.fn();
+
+ renderEditor({ dismissErrorToast, showError: true, errorText: 'Preview failed' });
+
+ userEvent.click(await screen.findByRole('button', { name: /Close/i }));
+
+ expect(dismissErrorToast).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders the hidden value textarea when editable', async () => {
+ renderEditor({ readOnly: false, value: 'editor value' });
+
+ await screen.findByRole('tab', { name: 'Editor' });
+
+ expect(screen.getByDisplayValue('editor value')).toBeInTheDocument();
});
- it('textarea disappears if readOnly', async () => {
- const props = {
- ...editorOptions,
- ...didMountStubs(),
- selectedView: 'input',
- };
- const wrapper = mount();
- await act(async () => jest.advanceTimersByTime(1000));
- expect(wrapper.find('textarea.hidden').exists()).toBe(true);
- wrapper.setProps({ readOnly: true });
- expect(wrapper.find('textarea.hidden').exists()).toBe(false);
+
+ it('hides the value textarea when read only', async () => {
+ const initializeEditor = jest.fn();
+ const { rerender } = renderWithI18n(
+
+ );
+
+ await screen.findByRole('tab', { name: 'Editor' });
+ expect(screen.getByDisplayValue('editor value')).toBeInTheDocument();
+
+ rerender(
+
+ );
+
+ expect(screen.queryByDisplayValue('editor value')).not.toBeInTheDocument();
});
});
diff --git a/webpack/assets/javascripts/react_app/components/Editor/__tests__/__snapshots__/Editor.test.js.snap b/webpack/assets/javascripts/react_app/components/Editor/__tests__/__snapshots__/Editor.test.js.snap
deleted file mode 100644
index 8ca4bdb24e..0000000000
--- a/webpack/assets/javascripts/react_app/components/Editor/__tests__/__snapshots__/Editor.test.js.snap
+++ /dev/null
@@ -1,173 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Editor rendering renders editor 1`] = `
-
-
- }
- className=""
- id="preview_error_toast"
- ouiaId="preview_error_toast"
- title=""
- variant="danger"
- />
-
-
-
-
-
-
-
-
-
-`;
diff --git a/webpack/assets/javascripts/react_app/components/Editor/__tests__/integration.test.js b/webpack/assets/javascripts/react_app/components/Editor/__tests__/integration.test.js
index bc30f74c52..6bacf8842d 100644
--- a/webpack/assets/javascripts/react_app/components/Editor/__tests__/integration.test.js
+++ b/webpack/assets/javascripts/react_app/components/Editor/__tests__/integration.test.js
@@ -8,6 +8,12 @@ import * as EditorActions from '../EditorActions'
jest.mock('../../../redux/API');
+const expectTabSelected = (component, ouiaId) => {
+ expect(
+ component.find(`[data-ouia-component-id="${ouiaId}"]`).prop('aria-selected')
+ ).toBe(true);
+};
+
describe('Editor integration test', () => {
it('should flow', () => {
jest
@@ -21,18 +27,14 @@ describe('Editor integration test', () => {
);
integrationTestHelper.takeStoreSnapshot('initial state');
- const previewBtn = component.find('#preview-navitem').at(1);
+ const previewBtn = component.find('[data-ouia-component-id="preview-navitem"]');
previewBtn.simulate('click');
+ component.update();
integrationTestHelper.takeStoreAndLastActionSnapshot(
'switched to preview view'
);
- expect(
- component
- .find('li[role="presentation"]')
- .at(2)
- .hasClass('active')
- ).toBe(true);
+ expectTabSelected(component, 'preview-navitem');
IntegrationTestHelper.flushAllPromises();
component.update();
diff --git a/webpack/assets/javascripts/react_app/components/Editor/components/EditorNavbar.js b/webpack/assets/javascripts/react_app/components/Editor/components/EditorNavbar.js
index e17fa975f3..e533960a3a 100644
--- a/webpack/assets/javascripts/react_app/components/Editor/components/EditorNavbar.js
+++ b/webpack/assets/javascripts/react_app/components/Editor/components/EditorNavbar.js
@@ -1,10 +1,9 @@
/* eslint-disable max-lines */
-import React, { useMemo, useState } from 'react';
+import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
-import { Nav, Spinner, Alert, Button } from 'patternfly-react';
+import { Spinner, Tab, Tabs, TabTitleText } from '@patternfly/react-core';
import { translate as __ } from '../../../common/I18n';
import AutocompleteInput from '../../common/AutocompleteInput/AutocompleteInput';
-import EditorRadioButton from './EditorRadioButton';
import EditorOptions from './EditorOptions';
import EditorSafemodeCheckbox from './EditorSafemodeCheckbox';
@@ -28,7 +27,6 @@ const EditorNavbar = ({
previewTemplate,
isSafemodeEnabled,
renderPath,
- safemodeRenderPath,
revertChanges,
selectedHost,
selectedView,
@@ -44,28 +42,12 @@ const EditorNavbar = ({
toggleRenderView,
value,
templateKindId,
- renderedEditorValue,
- previewResult,
- searchQuery,
- onHostSelectToggle,
onHostSearch,
- onSearchClear,
- isSelectOpen,
- showError,
fetchAndPreview,
+ safemode,
+ selectedRenderPath,
+ handleSafeModeChange,
}) => {
- const [safemode, setSafemode] = useState(isSafemodeEnabled);
- const handleSafeModeChange = ({ currentTarget: { checked: newChecked } }) => {
- setSafemode(newChecked);
- const newRenderPath = newChecked ? safemodeRenderPath : renderPath;
- previewTemplate({
- host: selectedHost,
- renderPath: newRenderPath,
- templateKindId,
- });
- };
- const selectedRenderPath = safemode ? safemodeRenderPath : renderPath;
-
const hostOptions = useMemo(() => {
const pool = isSearchingHosts ? filteredHosts : hosts;
const mapped = Array.from(pool, h => ({
@@ -107,50 +89,63 @@ const EditorNavbar = ({
}
};
+ const handleTabSelect = (event, tabKey) => {
+ if (tabKey === selectedView) {
+ return;
+ }
+
+ if (tabKey === 'input') {
+ if (isRendering) toggleRenderView();
+ changeTab('input');
+ return;
+ }
+
+ if (tabKey === 'diff') {
+ changeTab('diff');
+ return;
+ }
+
+ if (tabKey === 'preview') {
+ if (!isRendering) toggleRenderView();
+ changeTab('preview');
+ if (selectedHost.id === '') {
+ fetchAndPreview(selectedRenderPath, templateKindId, !showHostSelector);
+ }
+ }
+ };
+
return (
-