diff --git a/packages/design-system/package.json b/packages/design-system/package.json index ec3248f1e167e..6b7ff38e692eb 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -49,6 +49,7 @@ "@storybook/vue": "^7.0.7", "@storybook/vue-webpack5": "^7.0.7", "@testing-library/jest-dom": "^5.16.5", + "@testing-library/user-event": "^14.4.3", "@testing-library/vue": "^5.8.3", "@types/markdown-it": "^12.2.3", "@types/markdown-it-emoji": "^2.0.2", diff --git a/packages/design-system/src/components/N8nSelect/__tests__/Select.spec.ts b/packages/design-system/src/components/N8nSelect/__tests__/Select.spec.ts index 1c4084d869fb2..87a96883081b0 100644 --- a/packages/design-system/src/components/N8nSelect/__tests__/Select.spec.ts +++ b/packages/design-system/src/components/N8nSelect/__tests__/Select.spec.ts @@ -1,4 +1,6 @@ -import { render } from '@testing-library/vue'; +import { defineComponent, ref } from 'vue'; +import { render, waitFor, within } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; import N8nSelect from '../Select.vue'; import N8nOption from '../../N8nOption/Option.vue'; @@ -19,5 +21,38 @@ describe('components', () => { }); expect(wrapper.html()).toMatchSnapshot(); }); + + it('should select an option', async () => { + const n8nSelectTestComponent = defineComponent({ + components: { + N8nSelect, + N8nOption, + }, + template: ` + + + + `, + setup() { + const options = ref(['1', '2', '3']); + const selected = ref(''); + + return { + options, + selected, + }; + }, + }); + + const { container, getByRole } = render(n8nSelectTestComponent); + const getOption = (value: string) => within(container as HTMLElement).getByText(value); + const textbox = getByRole('textbox'); + + await userEvent.click(textbox); + await waitFor(() => expect(getOption('1')).toBeVisible()); + await userEvent.click(getOption('1')); + + expect(textbox).toHaveValue('1'); + }); }); }); diff --git a/packages/editor-ui/src/__tests__/server/endpoints/index.ts b/packages/editor-ui/src/__tests__/server/endpoints/index.ts index d79a81d30883b..911ca9c11f6cc 100644 --- a/packages/editor-ui/src/__tests__/server/endpoints/index.ts +++ b/packages/editor-ui/src/__tests__/server/endpoints/index.ts @@ -5,6 +5,7 @@ import { routesForCredentialTypes } from './credentialType'; import { routesForVariables } from './variable'; import { routesForSettings } from './settings'; import { routesForSSO } from './sso'; +import { routesForVersionControl } from './versionControl'; const endpoints: Array<(server: Server) => void> = [ routesForCredentials, @@ -13,6 +14,7 @@ const endpoints: Array<(server: Server) => void> = [ routesForVariables, routesForSettings, routesForSSO, + routesForVersionControl, ]; export { endpoints }; diff --git a/packages/editor-ui/src/__tests__/server/endpoints/versionControl.ts b/packages/editor-ui/src/__tests__/server/endpoints/versionControl.ts new file mode 100644 index 0000000000000..09a079d8691d4 --- /dev/null +++ b/packages/editor-ui/src/__tests__/server/endpoints/versionControl.ts @@ -0,0 +1,77 @@ +import type { Server, Request } from 'miragejs'; +import { Response } from 'miragejs'; +import { jsonParse } from 'n8n-workflow'; +import type { AppSchema } from '@/__tests__/server/types'; +import type { VersionControlPreferences } from '@/Interface'; + +export function routesForVersionControl(server: Server) { + const versionControlApiRoot = '/rest/version-control'; + const defaultVersionControlPreferences: VersionControlPreferences = { + branchName: '', + branches: [], + authorName: '', + authorEmail: '', + repositoryUrl: '', + branchReadOnly: false, + branchColor: '#1d6acb', + connected: false, + publicKey: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHEX+25m', + }; + + server.post(`${versionControlApiRoot}/preferences`, (schema: AppSchema, request: Request) => { + const requestBody = jsonParse(request.requestBody) as Partial; + + return new Response( + 200, + {}, + { + data: { + ...defaultVersionControlPreferences, + ...requestBody, + }, + }, + ); + }); + + server.patch(`${versionControlApiRoot}/preferences`, (schema: AppSchema, request: Request) => { + const requestBody = jsonParse(request.requestBody) as Partial; + + return new Response( + 200, + {}, + { + data: { + ...defaultVersionControlPreferences, + ...requestBody, + }, + }, + ); + }); + + server.get(`${versionControlApiRoot}/get-branches`, () => { + return new Response( + 200, + {}, + { + data: { + branches: ['main', 'dev'], + currentBranch: 'main', + }, + }, + ); + }); + + server.post(`${versionControlApiRoot}/disconnect`, () => { + return new Response( + 200, + {}, + { + data: { + ...defaultVersionControlPreferences, + branchName: '', + connected: false, + }, + }, + ); + }); +} diff --git a/packages/editor-ui/src/__tests__/server/index.ts b/packages/editor-ui/src/__tests__/server/index.ts index f0dea12024c29..18ea0a4be8b67 100644 --- a/packages/editor-ui/src/__tests__/server/index.ts +++ b/packages/editor-ui/src/__tests__/server/index.ts @@ -23,14 +23,14 @@ export function setupServer() { // Enable logging server.logging = false; - // Handle undefined endpoints - server.post('/rest/:any', async () => new Promise(() => {})); - // Handle defined endpoints for (const endpointsFn of endpoints) { endpointsFn(server); } + // Handle undefined endpoints + server.post('/rest/:any', async () => ({})); + // Reset for everything else server.namespace = ''; server.passthrough(); diff --git a/packages/editor-ui/src/views/SettingsVersionControl.vue b/packages/editor-ui/src/views/SettingsVersionControl.vue index 066b37493596c..0fb9a0c09c839 100644 --- a/packages/editor-ui/src/views/SettingsVersionControl.vue +++ b/packages/editor-ui/src/views/SettingsVersionControl.vue @@ -219,6 +219,7 @@ const refreshBranches = async () => { @click="onDisconnect" size="large" icon="trash" + data-test-id="version-control-disconnect-button" >{{ locale.baseText('settings.versionControl.button.disconnect') }} @@ -291,9 +292,10 @@ const refreshBranches = async () => { size="large" :disabled="!validForConnection" :class="$style.connect" + data-test-id="version-control-connect-button" >{{ locale.baseText('settings.versionControl.button.connect') }} -
+

{{ @@ -307,6 +309,7 @@ const refreshBranches = async () => { size="medium" filterable @input="onSelect" + data-test-id="version-control-branch-select" > { square :class="$style.refreshBranches" @click="refreshBranches" + data-test-id="version-control-refresh-branches-button" />
@@ -358,6 +362,7 @@ const refreshBranches = async () => { @click="onSave" size="large" :disabled="!versionControlStore.preferences.branchName" + data-test-id="version-control-save-settings-button" >{{ locale.baseText('settings.versionControl.button.save') }}
diff --git a/packages/editor-ui/src/views/__tests__/SettingsVersionControl.test.ts b/packages/editor-ui/src/views/__tests__/SettingsVersionControl.test.ts index 73123d13d1195..55fa1027debbf 100644 --- a/packages/editor-ui/src/views/__tests__/SettingsVersionControl.test.ts +++ b/packages/editor-ui/src/views/__tests__/SettingsVersionControl.test.ts @@ -1,14 +1,16 @@ -import { PiniaVuePlugin } from 'pinia'; -import { render } from '@testing-library/vue'; -import { createTestingPinia } from '@pinia/testing'; +import { vi } from 'vitest'; +import { screen, render, waitFor, within } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; +import { createPinia, setActivePinia, PiniaVuePlugin } from 'pinia'; import { merge } from 'lodash-es'; -import { STORES } from '@/constants'; -import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils'; -import { i18n } from '@/plugins/i18n'; +import { setupServer } from '@/__tests__/server'; +import { i18nInstance } from '@/plugins/i18n'; +import { useSettingsStore, useVersionControlStore } from '@/stores'; import SettingsVersionControl from '@/views/SettingsVersionControl.vue'; -import { useVersionControlStore } from '@/stores/versionControl.store'; -let pinia: ReturnType; +let pinia: ReturnType; +let server: ReturnType; +let settingsStore: ReturnType; let versionControlStore: ReturnType; const renderComponent = (renderOptions: Parameters[1] = {}) => @@ -17,7 +19,7 @@ const renderComponent = (renderOptions: Parameters[1] = {}) => merge( { pinia, - i18n, + i18n: i18nInstance, }, renderOptions, ), @@ -27,22 +29,28 @@ const renderComponent = (renderOptions: Parameters[1] = {}) => ); describe('SettingsVersionControl', () => { + beforeAll(() => { + server = setupServer(); + }); + beforeEach(() => { - pinia = createTestingPinia({ - initialState: { - [STORES.SETTINGS]: { - settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings), - }, - }, - }); - versionControlStore = useVersionControlStore(pinia); + pinia = createPinia(); + setActivePinia(pinia); + settingsStore = useSettingsStore(); + versionControlStore = useVersionControlStore(); }); afterEach(() => { vi.clearAllMocks(); }); + afterAll(() => { + server.shutdown(); + }); + it('should render paywall state when there is no license', () => { + vi.spyOn(settingsStore, 'isEnterpriseFeatureEnabled').mockReturnValue(false); + const { getByTestId, queryByTestId } = renderComponent(); expect(queryByTestId('version-control-content-licensed')).not.toBeInTheDocument(); @@ -50,11 +58,86 @@ describe('SettingsVersionControl', () => { }); it('should render licensed content', () => { - vi.spyOn(versionControlStore, 'isEnterpriseVersionControlEnabled', 'get').mockReturnValue(true); + vi.spyOn(settingsStore, 'isEnterpriseFeatureEnabled').mockReturnValue(true); const { getByTestId, queryByTestId } = renderComponent(); expect(getByTestId('version-control-content-licensed')).toBeInTheDocument(); expect(queryByTestId('version-control-content-unlicensed')).not.toBeInTheDocument(); + expect(queryByTestId('version-control-connected-content')).not.toBeInTheDocument(); + }); + + it('should render user flow happy path', async () => { + vi.spyOn(settingsStore, 'isEnterpriseFeatureEnabled').mockReturnValue(true); + const updatePreferencesSpy = vi.spyOn(versionControlStore, 'updatePreferences'); + + const { container, getByTestId, queryByTestId, getByRole } = renderComponent(); + + const connectButton = getByTestId('version-control-connect-button'); + expect(connectButton).toBeDisabled(); + + const repoUrlInput = container.querySelector('input[name="repoUrl"]')!; + const authorName = container.querySelector('input[name="authorName"]')!; + const authorEmail = container.querySelector('input[name="authorEmail"]')!; + + await userEvent.click(repoUrlInput); + await userEvent.type(repoUrlInput, 'git@github'); + await userEvent.tab(); + expect(connectButton).toBeDisabled(); + + await userEvent.click(repoUrlInput); + await userEvent.type(repoUrlInput, '.com:john/n8n-data.git'); + await userEvent.tab(); + expect(connectButton).toBeDisabled(); + + await userEvent.click(authorName); + await userEvent.type(authorName, 'John Doe'); + await userEvent.tab(); + expect(connectButton).toBeDisabled(); + + await userEvent.click(authorEmail); + await userEvent.type(authorEmail, 'john@example.'); + await userEvent.tab(); + expect(connectButton).toBeDisabled(); + + await userEvent.click(authorEmail); + await userEvent.type(authorEmail, 'com'); + await userEvent.tab(); + + expect(connectButton).toBeEnabled(); + expect(queryByTestId('version-control-save-settings-button')).not.toBeInTheDocument(); + + await userEvent.click(connectButton); + await waitFor(() => expect(getByTestId('version-control-connected-content')).toBeVisible()); + + const saveSettingsButton = getByTestId('version-control-save-settings-button'); + expect(saveSettingsButton).toBeInTheDocument(); + expect(saveSettingsButton).toBeDisabled(); + + const branchSelect = getByTestId('version-control-branch-select'); + await userEvent.click(within(branchSelect).getByRole('textbox')); + + await waitFor(() => expect(within(branchSelect).getByText('main')).toBeVisible()); + await userEvent.click(within(branchSelect).getByText('main')); + + await waitFor(() => expect(saveSettingsButton).toBeEnabled()); + await userEvent.click(saveSettingsButton); + + expect(updatePreferencesSpy).toHaveBeenCalledWith({ + branchName: 'main', + branchReadOnly: false, + branchColor: '#1d6acb', + }); + await waitFor(() => expect(screen.getByText('Settings successfully saved')).toBeVisible()); + + await userEvent.click(getByTestId('version-control-disconnect-button')); + const disconnectDialog = getByRole('dialog'); + await waitFor(() => expect(disconnectDialog).toBeVisible()); + + await userEvent.click(within(disconnectDialog).getAllByRole('button')[1]); + await waitFor(() => expect(disconnectDialog).not.toBeVisible()); + await waitFor(() => + expect(queryByTestId('version-control-connected-content')).not.toBeInTheDocument(), + ); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 379e77e174120..08392b8c471e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,7 +142,7 @@ importers: dependencies: axios: specifier: ^0.21.1 - version: 0.21.4(debug@4.3.2) + version: 0.21.4 packages/@n8n_io/eslint-config: devDependencies: @@ -223,7 +223,7 @@ importers: version: 7.28.1 axios: specifier: ^0.21.1 - version: 0.21.4(debug@4.3.2) + version: 0.21.4 basic-auth: specifier: ^2.0.1 version: 2.0.1 @@ -656,7 +656,7 @@ importers: version: link:../@n8n/client-oauth2 axios: specifier: ^0.21.1 - version: 0.21.4(debug@4.3.2) + version: 0.21.4 concat-stream: specifier: ^2.0.0 version: 2.0.0 @@ -803,6 +803,9 @@ importers: '@testing-library/jest-dom': specifier: ^5.16.5 version: 5.16.5 + '@testing-library/user-event': + specifier: ^14.4.3 + version: 14.4.3(@testing-library/dom@7.31.2) '@testing-library/vue': specifier: ^5.8.3 version: 5.8.3(vue-template-compiler@2.7.14)(vue@2.7.14) @@ -904,7 +907,7 @@ importers: version: 5.13.2 axios: specifier: ^0.21.1 - version: 0.21.4(debug@4.3.2) + version: 0.21.4 codemirror-lang-html-n8n: specifier: ^1.0.0 version: 1.0.0 @@ -5631,6 +5634,38 @@ packages: dev: false optional: true + /@oclif/command@1.8.18(@oclif/config@1.18.2): + resolution: {integrity: sha512-qTad+jtiriMMbkw6ArtcUY89cwLwmwDnD4KSGT+OQiZKYtegp3NUCM9JN8lfj/aKC+0kvSitJM4ULzbgiVTKQQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@oclif/config': ^1 + dependencies: + '@oclif/config': 1.18.2 + '@oclif/errors': 1.3.6 + '@oclif/help': 1.0.3(supports-color@8.1.1) + '@oclif/parser': 3.8.8 + debug: 4.3.4(supports-color@8.1.1) + semver: 7.3.8 + transitivePeerDependencies: + - supports-color + dev: true + + /@oclif/command@1.8.18(@oclif/config@1.18.5): + resolution: {integrity: sha512-qTad+jtiriMMbkw6ArtcUY89cwLwmwDnD4KSGT+OQiZKYtegp3NUCM9JN8lfj/aKC+0kvSitJM4ULzbgiVTKQQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@oclif/config': ^1 + dependencies: + '@oclif/config': 1.18.5(supports-color@8.1.1) + '@oclif/errors': 1.3.6 + '@oclif/help': 1.0.3(supports-color@8.1.1) + '@oclif/parser': 3.8.8 + debug: 4.3.4(supports-color@8.1.1) + semver: 7.3.8 + transitivePeerDependencies: + - supports-color + dev: true + /@oclif/command@1.8.18(@oclif/config@1.18.5)(supports-color@8.1.1): resolution: {integrity: sha512-qTad+jtiriMMbkw6ArtcUY89cwLwmwDnD4KSGT+OQiZKYtegp3NUCM9JN8lfj/aKC+0kvSitJM4ULzbgiVTKQQ==} engines: {node: '>=12.0.0'} @@ -5712,7 +5747,7 @@ packages: engines: {node: '>=8.10.0'} hasBin: true dependencies: - '@oclif/command': 1.8.18(@oclif/config@1.18.5)(supports-color@8.1.1) + '@oclif/command': 1.8.18(@oclif/config@1.18.5) '@oclif/config': 1.18.5(supports-color@8.1.1) '@oclif/errors': 1.3.6 '@oclif/plugin-help': 3.2.18 @@ -5782,7 +5817,7 @@ packages: resolution: {integrity: sha512-5n5Pkz4L0duknIvFwx2Ko9Xda3miT6RZP8bgaaK3Q/9fzVBrhi4bOM0u05/OThI6V+3NsSdxYS2o1NLcXToWDg==} engines: {node: '>=8.0.0'} dependencies: - '@oclif/command': 1.8.18(@oclif/config@1.18.5)(supports-color@8.1.1) + '@oclif/command': 1.8.18(@oclif/config@1.18.2) '@oclif/config': 1.18.2 '@oclif/errors': 1.3.5 '@oclif/help': 1.0.3(supports-color@8.1.1) @@ -5842,7 +5877,7 @@ packages: dependencies: '@segment/loosely-validate-event': 2.0.0 auto-changelog: 1.16.4 - axios: 0.21.4(debug@4.3.2) + axios: 0.21.4 axios-retry: 3.3.1 bull: 3.29.3 lodash.clonedeep: 4.5.0 @@ -9673,6 +9708,14 @@ packages: is-retry-allowed: 2.2.0 dev: false + /axios@0.21.4: + resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} + dependencies: + follow-redirects: 1.15.2(debug@3.2.7) + transitivePeerDependencies: + - debug + dev: false + /axios@0.21.4(debug@4.3.2): resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} dependencies: @@ -10998,6 +11041,7 @@ packages: /consolidate@0.15.1(react-dom@18.2.0)(react@17.0.2): resolution: {integrity: sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==} engines: {node: '>= 0.10.0'} + deprecated: Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog peerDependencies: arc-templates: ^0.5.3 atpl: '>=0.7.6'