diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-16 15:09:26 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-16 15:09:26 +0300 |
commit | b019dc959ec16b15fe42a680dbd729542ff61537 (patch) | |
tree | cde5c3d0582bd7542e1e195d83ad7a50079f4e49 /spec | |
parent | b7b44de429911864686599ef1643baf525bf75ec (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
45 files changed, 802 insertions, 177 deletions
diff --git a/spec/factories/incident_management/timeline_events.rb b/spec/factories/incident_management/timeline_events.rb index e2e216d24b8..831f78369b7 100644 --- a/spec/factories/incident_management/timeline_events.rb +++ b/spec/factories/incident_management/timeline_events.rb @@ -10,5 +10,14 @@ FactoryBot.define do note { 'timeline created' } note_html { '<strong>timeline created</strong>' } action { 'comment' } + editable + end + + trait :editable do + editable { true } + end + + trait :non_editable do + editable { false } end end diff --git a/spec/factories/releases.rb b/spec/factories/releases.rb index 52a9341b955..a07d4ef6c2e 100644 --- a/spec/factories/releases.rb +++ b/spec/factories/releases.rb @@ -14,7 +14,11 @@ FactoryBot.define do trait :legacy do sha { nil } - author { nil } + + # Legacy releases which are created during tags creation have empty users. + after(:create) do |release, _| + release.update_column(:author_id, nil) + end end trait :with_evidence do diff --git a/spec/finders/crm/contacts_finder_spec.rb b/spec/finders/crm/contacts_finder_spec.rb index 14f838812a6..fee4f049123 100644 --- a/spec/finders/crm/contacts_finder_spec.rb +++ b/spec/finders/crm/contacts_finder_spec.rb @@ -143,6 +143,14 @@ RSpec.describe Crm::ContactsFinder do expect(finder.execute).to match_array([search_test_b]) end end + + context 'when searching for contacts ids' do + it 'returns the expected contacts' do + finder = described_class.new(user, group: search_test_group, ids: [search_test_b.id]) + + expect(finder.execute).to match_array([search_test_b]) + end + end end end end diff --git a/spec/finders/crm/organizations_finder_spec.rb b/spec/finders/crm/organizations_finder_spec.rb index 94b5d9e5874..807c9f36484 100644 --- a/spec/finders/crm/organizations_finder_spec.rb +++ b/spec/finders/crm/organizations_finder_spec.rb @@ -129,6 +129,14 @@ RSpec.describe Crm::OrganizationsFinder do expect(finder.execute).to match_array([search_test_b]) end end + + context 'when searching for organizations ids' do + it 'returns the expected organizations' do + finder = described_class.new(user, group: search_test_group, ids: [search_test_a.id]) + + expect(finder.execute).to match_array([search_test_a]) + end + end end end end diff --git a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js index 5b4f954b672..6a859873a9d 100644 --- a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js +++ b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js @@ -16,6 +16,9 @@ describe('Signup Form', () => { wrapper = extendedWrapper( mountFn(SignupForm, { provide: { + glFeatures: { + passwordComplexity: true, + }, ...mockData, ...injectedProps, }, @@ -58,6 +61,10 @@ describe('Signup Form', () => { ${'minimumPasswordLength'} | ${mockData.minimumPasswordLength} | ${'[name="application_setting[minimum_password_length]"]'} | ${'attribute'} | ${'value'} | ${mockData.minimumPasswordLength} ${'minimumPasswordLengthMin'} | ${mockData.minimumPasswordLengthMin} | ${'[name="application_setting[minimum_password_length]"]'} | ${'attribute'} | ${'min'} | ${mockData.minimumPasswordLengthMin} ${'minimumPasswordLengthMax'} | ${mockData.minimumPasswordLengthMax} | ${'[name="application_setting[minimum_password_length]"]'} | ${'attribute'} | ${'max'} | ${mockData.minimumPasswordLengthMax} + ${'passwordNumberRequired'} | ${mockData.passwordNumberRequired} | ${'[name="application_setting[password_number_required]"]'} | ${'prop'} | ${'value'} | ${mockData.passwordNumberRequired} + ${'passwordLowercaseRequired'} | ${mockData.passwordLowercaseRequired} | ${'[name="application_setting[password_lowercase_required]"]'} | ${'prop'} | ${'value'} | ${mockData.passwordLowercaseRequired} + ${'passwordUppercaseRequired'} | ${mockData.passwordUppercaseRequired} | ${'[name="application_setting[password_uppercase_required]"]'} | ${'prop'} | ${'value'} | ${mockData.passwordUppercaseRequired} + ${'passwordSymbolRequired'} | ${mockData.passwordSymbolRequired} | ${'[name="application_setting[password_symbol_required]"]'} | ${'prop'} | ${'value'} | ${mockData.passwordSymbolRequired} ${'domainAllowlistRaw'} | ${mockData.domainAllowlistRaw} | ${'[name="application_setting[domain_allowlist_raw]"]'} | ${'value'} | ${'value'} | ${mockData.domainAllowlistRaw} ${'domainDenylistEnabled'} | ${mockData.domainDenylistEnabled} | ${'[name="application_setting[domain_denylist_enabled]"]'} | ${'prop'} | ${'value'} | ${mockData.domainDenylistEnabled} ${'denylistTypeRawSelected'} | ${mockData.denylistTypeRawSelected} | ${'[name="denylist_type"]'} | ${'attribute'} | ${'checked'} | ${'raw'} diff --git a/spec/frontend/admin/signup_restrictions/mock_data.js b/spec/frontend/admin/signup_restrictions/mock_data.js index 135fc8caae0..9e001e122a4 100644 --- a/spec/frontend/admin/signup_restrictions/mock_data.js +++ b/spec/frontend/admin/signup_restrictions/mock_data.js @@ -18,6 +18,10 @@ export const rawMockData = { emailRestrictions: 'user1@domain.com, user2@domain.com', afterSignUpText: 'Congratulations on your successful sign-up!', pendingUserCount: '0', + passwordNumberRequired: 'true', + passwordLowercaseRequired: 'true', + passwordUppercaseRequired: 'true', + passwordSymbolRequired: 'true', }; export const mockData = { @@ -40,4 +44,8 @@ export const mockData = { emailRestrictions: 'user1@domain.com, user2@domain.com', afterSignUpText: 'Congratulations on your successful sign-up!', pendingUserCount: '0', + passwordNumberRequired: true, + passwordLowercaseRequired: true, + passwordUppercaseRequired: true, + passwordSymbolRequired: true, }; diff --git a/spec/frontend/admin/signup_restrictions/utils_spec.js b/spec/frontend/admin/signup_restrictions/utils_spec.js index fd5c4c3317b..f07e14430f9 100644 --- a/spec/frontend/admin/signup_restrictions/utils_spec.js +++ b/spec/frontend/admin/signup_restrictions/utils_spec.js @@ -14,6 +14,10 @@ describe('utils', () => { 'domainDenylistEnabled', 'denylistTypeRawSelected', 'emailRestrictionsEnabled', + 'passwordNumberRequired', + 'passwordLowercaseRequired', + 'passwordUppercaseRequired', + 'passwordSymbolRequired', ], }), ).toEqual(mockData); diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index 5f162f498c4..9526277f06b 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; import Api, { DEFAULT_PER_PAGE } from '~/api'; import axios from '~/lib/utils/axios_utils'; import httpStatus from '~/lib/utils/http_status'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; jest.mock('~/flash'); @@ -622,8 +622,8 @@ describe('Api', () => { const query = 'dummy query'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`; const flashCallback = (callCount) => { - expect(createFlash).toHaveBeenCalledTimes(callCount); - createFlash.mockClear(); + expect(createAlert).toHaveBeenCalledTimes(callCount); + createAlert.mockClear(); }; mock.onGet(expectedUrl).reply(500, null); diff --git a/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js b/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js index f19bd02443f..646d068e795 100644 --- a/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js +++ b/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js @@ -12,6 +12,7 @@ import { stubComponent } from 'helpers/stub_component'; import CodeBlockBubbleMenu from '~/content_editor/components/bubble_menus/code_block.vue'; import eventHubFactory from '~/helpers/event_hub_factory'; import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight'; +import Diagram from '~/content_editor/extensions/diagram'; import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader'; import { createTestEditor, emitEditorEvent } from '../../test_utils'; @@ -20,11 +21,13 @@ const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jes describe('content_editor/components/bubble_menus/code_block', () => { let wrapper; let tiptapEditor; + let contentEditor; let bubbleMenu; let eventHub; const buildEditor = () => { - tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight] }); + tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight, Diagram] }); + contentEditor = { renderDiagram: jest.fn() }; eventHub = eventHubFactory(); }; @@ -32,6 +35,7 @@ describe('content_editor/components/bubble_menus/code_block', () => { wrapper = mountExtended(CodeBlockBubbleMenu, { provide: { tiptapEditor, + contentEditor, eventHub, }, stubs: { @@ -85,6 +89,15 @@ describe('content_editor/components/bubble_menus/code_block', () => { expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Javascript'); }); + it('selects diagram sytnax for mermaid', async () => { + tiptapEditor.commands.insertContent('<pre lang="mermaid">test</pre>'); + bubbleMenu = wrapper.findComponent(BubbleMenu); + + await emitEditorEvent({ event: 'transaction', tiptapEditor }); + + expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Diagram (mermaid)'); + }); + it("selects Custom (syntax) if the language doesn't exist in the list", async () => { tiptapEditor.commands.insertContent('<pre lang="nomnoml">test</pre>'); bubbleMenu = wrapper.findComponent(BubbleMenu); @@ -116,6 +129,39 @@ describe('content_editor/components/bubble_menus/code_block', () => { }); }); + describe('preview button', () => { + it('does not appear for a regular code block', async () => { + tiptapEditor.commands.insertContent('<pre lang="javascript">var a = 2;</pre>'); + + expect(wrapper.findByTestId('preview-diagram').exists()).toBe(false); + }); + + it.each` + diagramType | diagramCode + ${'mermaid'} | ${'<pre lang="mermaid">graph TD;\n A-->B;</pre>'} + ${'nomnoml'} | ${'<img data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,WzxmcmFtZT5EZWNvcmF0b3IgcGF0dGVybl0=">'} + `('toggles preview for a $diagramType diagram', async ({ diagramType, diagramCode }) => { + tiptapEditor.commands.insertContent(diagramCode); + + await nextTick(); + await wrapper.findByTestId('preview-diagram').vm.$emit('click'); + + expect(tiptapEditor.getAttributes(Diagram.name)).toEqual({ + isDiagram: true, + language: diagramType, + showPreview: false, + }); + + await wrapper.findByTestId('preview-diagram').vm.$emit('click'); + + expect(tiptapEditor.getAttributes(Diagram.name)).toEqual({ + isDiagram: true, + language: diagramType, + showPreview: true, + }); + }); + }); + describe('when opened and search is changed', () => { beforeEach(async () => { tiptapEditor.commands.insertContent('<pre lang="javascript">var a = 2;</pre>'); diff --git a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js new file mode 100644 index 00000000000..0334a18c9a1 --- /dev/null +++ b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js @@ -0,0 +1,54 @@ +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import ToolbarMoreDropdown from '~/content_editor/components/toolbar_more_dropdown.vue'; +import Diagram from '~/content_editor/extensions/diagram'; +import HorizontalRule from '~/content_editor/extensions/horizontal_rule'; +import { createTestEditor, mockChainedCommands } from '../test_utils'; + +describe('content_editor/components/toolbar_more_dropdown', () => { + let wrapper; + let tiptapEditor; + + const buildEditor = () => { + tiptapEditor = createTestEditor({ + extensions: [Diagram, HorizontalRule], + }); + }; + + const buildWrapper = (propsData = {}) => { + wrapper = mountExtended(ToolbarMoreDropdown, { + provide: { + tiptapEditor, + }, + propsData, + }); + }; + + beforeEach(() => { + buildEditor(); + buildWrapper(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe.each` + label | contentType | data + ${'Mermaid diagram'} | ${'diagram'} | ${{ language: 'mermaid' }} + ${'PlantUML diagram'} | ${'diagram'} | ${{ language: 'plantuml' }} + ${'Horizontal rule'} | ${'horizontalRule'} | ${undefined} + `('when option $label is clicked', ({ label, contentType, data }) => { + it(`inserts a ${contentType}`, async () => { + const commands = mockChainedCommands(tiptapEditor, ['setNode', 'focus', 'run']); + + const btn = wrapper.findByRole('menuitem', { name: label }); + await btn.trigger('click'); + + expect(commands.focus).toHaveBeenCalled(); + expect(commands.setNode).toHaveBeenCalledWith(contentType, data); + expect(commands.run).toHaveBeenCalled(); + + expect(wrapper.emitted('execute')).toEqual([[{ contentType }]]); + }); + }); +}); diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/top_toolbar_spec.js index ec58877470c..d98a9a52aff 100644 --- a/spec/frontend/content_editor/components/top_toolbar_spec.js +++ b/spec/frontend/content_editor/components/top_toolbar_spec.js @@ -23,20 +23,21 @@ describe('content_editor/components/top_toolbar', () => { }); describe.each` - testId | controlProps - ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold' }} - ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic' }} - ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike' }} - ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }} - ${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote', editorCommand: 'toggleBlockquote' }} - ${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }} - ${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }} - ${'details'} | ${{ contentType: 'details', iconName: 'details-block', label: 'Add a collapsible section', editorCommand: 'toggleDetails' }} - ${'horizontal-rule'} | ${{ contentType: 'horizontalRule', iconName: 'dash', label: 'Add a horizontal rule', editorCommand: 'setHorizontalRule' }} - ${'code-block'} | ${{ contentType: 'codeBlock', iconName: 'doc-code', label: 'Insert a code block', editorCommand: 'toggleCodeBlock' }} - ${'text-styles'} | ${{}} - ${'link'} | ${{}} - ${'image'} | ${{}} + testId | controlProps + ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold' }} + ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic' }} + ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike' }} + ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }} + ${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote', editorCommand: 'toggleBlockquote' }} + ${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }} + ${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }} + ${'details'} | ${{ contentType: 'details', iconName: 'details-block', label: 'Add a collapsible section', editorCommand: 'toggleDetails' }} + ${'code-block'} | ${{ contentType: 'codeBlock', iconName: 'doc-code', label: 'Insert a code block', editorCommand: 'toggleCodeBlock' }} + ${'text-styles'} | ${{}} + ${'link'} | ${{}} + ${'image'} | ${{}} + ${'table'} | ${{}} + ${'more'} | ${{}} `('given a $testId toolbar control', ({ testId, controlProps }) => { beforeEach(() => { buildWrapper(); diff --git a/spec/frontend/content_editor/components/wrappers/code_block_spec.js b/spec/frontend/content_editor/components/wrappers/code_block_spec.js index 2e59cd9714a..17a365e12bb 100644 --- a/spec/frontend/content_editor/components/wrappers/code_block_spec.js +++ b/spec/frontend/content_editor/components/wrappers/code_block_spec.js @@ -1,8 +1,14 @@ import { nextTick } from 'vue'; import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2'; -import { shallowMount } from '@vue/test-utils'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { stubComponent } from 'helpers/stub_component'; +import eventHubFactory from '~/helpers/event_hub_factory'; +import SandboxedMermaid from '~/behaviors/components/sandboxed_mermaid.vue'; +import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight'; +import Diagram from '~/content_editor/extensions/diagram'; import CodeBlockWrapper from '~/content_editor/components/wrappers/code_block.vue'; import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader'; +import { emitEditorEvent, createTestEditor } from '../../test_utils'; jest.mock('~/content_editor/services/code_block_language_loader'); @@ -10,21 +16,42 @@ describe('content/components/wrappers/code_block', () => { const language = 'yaml'; let wrapper; let updateAttributesFn; + let tiptapEditor; + let contentEditor; + let eventHub; + + const buildEditor = () => { + tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight, Diagram] }); + contentEditor = { renderDiagram: jest.fn().mockResolvedValue('url/to/some/diagram') }; + eventHub = eventHubFactory(); + }; const createWrapper = async (nodeAttrs = { language }) => { updateAttributesFn = jest.fn(); - wrapper = shallowMount(CodeBlockWrapper, { + wrapper = mountExtended(CodeBlockWrapper, { propsData: { + editor: tiptapEditor, node: { attrs: nodeAttrs, }, updateAttributes: updateAttributesFn, }, + stubs: { + NodeViewContent: stubComponent(NodeViewContent), + NodeViewWrapper: stubComponent(NodeViewWrapper), + }, + provide: { + contentEditor, + tiptapEditor, + eventHub, + }, }); }; beforeEach(() => { + buildEditor(); + codeBlockLanguageLoader.findOrCreateLanguageBySyntax.mockReturnValue({ syntax: language }); }); @@ -68,4 +95,56 @@ describe('content/components/wrappers/code_block', () => { expect(updateAttributesFn).toHaveBeenCalledWith({ language }); }); + + describe('diagrams', () => { + beforeEach(() => { + jest.spyOn(tiptapEditor, 'isActive').mockReturnValue(true); + }); + + it('does not render a preview if showPreview: false', async () => { + createWrapper({ language: 'plantuml', isDiagram: true, showPreview: false }); + + expect(wrapper.find({ ref: 'diagramContainer' }).exists()).toBe(false); + }); + + it('does not update preview when diagram is not active', async () => { + createWrapper({ language: 'plantuml', isDiagram: true, showPreview: true }); + + await emitEditorEvent({ event: 'transaction', tiptapEditor }); + await nextTick(); + + expect(wrapper.find('img').attributes('src')).toBe('url/to/some/diagram'); + + jest.spyOn(tiptapEditor, 'isActive').mockReturnValue(false); + + const alternateUrl = 'url/to/another/diagram'; + + contentEditor.renderDiagram.mockResolvedValue(alternateUrl); + + await emitEditorEvent({ event: 'transaction', tiptapEditor }); + await nextTick(); + + expect(wrapper.find('img').attributes('src')).toBe('url/to/some/diagram'); + }); + + it('renders an image with preview for a plantuml/kroki diagram', async () => { + createWrapper({ language: 'plantuml', isDiagram: true, showPreview: true }); + + await emitEditorEvent({ event: 'transaction', tiptapEditor }); + await nextTick(); + + expect(wrapper.find('img').attributes('src')).toBe('url/to/some/diagram'); + expect(wrapper.find(SandboxedMermaid).exists()).toBe(false); + }); + + it('renders an iframe with preview for a mermaid diagram', async () => { + createWrapper({ language: 'mermaid', isDiagram: true, showPreview: true }); + + await emitEditorEvent({ event: 'transaction', tiptapEditor }); + await nextTick(); + + expect(wrapper.find(SandboxedMermaid).props('source')).toBe(''); + expect(wrapper.find('img').exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/content_editor/services/asset_resolver_spec.js b/spec/frontend/content_editor/services/asset_resolver_spec.js index f4e7d9bf881..0a99f823be3 100644 --- a/spec/frontend/content_editor/services/asset_resolver_spec.js +++ b/spec/frontend/content_editor/services/asset_resolver_spec.js @@ -20,4 +20,14 @@ describe('content_editor/services/asset_resolver', () => { ); }); }); + + describe('renderDiagram', () => { + it('resolves a diagram code to a url containing the diagram image', async () => { + renderMarkdown.mockResolvedValue( + '<p><img data-diagram="nomnoml" src="url/to/some/diagram"></p>', + ); + + expect(await assetResolver.renderDiagram('test')).toBe('url/to/some/diagram'); + }); + }); }); diff --git a/spec/frontend/content_editor/services/code_block_language_loader_spec.js b/spec/frontend/content_editor/services/code_block_language_loader_spec.js index 9b2600f85d9..795f5219a3f 100644 --- a/spec/frontend/content_editor/services/code_block_language_loader_spec.js +++ b/spec/frontend/content_editor/services/code_block_language_loader_spec.js @@ -35,6 +35,13 @@ describe('content_editor/services/code_block_language_loader', () => { }); }); + it('returns Diagram (syntax) if the language does not exist, and isDiagram = true', () => { + expect(languageLoader.findOrCreateLanguageBySyntax('foobar', true)).toMatchObject({ + syntax: 'foobar', + label: 'Diagram (foobar)', + }); + }); + it('returns plaintext if no syntax is passed', () => { expect(languageLoader.findOrCreateLanguageBySyntax('')).toMatchObject({ syntax: 'plaintext', diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js index 7a71a1cea0f..4f3d780b149 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js @@ -1,4 +1,9 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlAlert } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { conanMetadata, mavenMetadata, @@ -6,9 +11,11 @@ import { packageData, composerMetadata, pypiMetadata, + packageMetadataQuery, } from 'jest/packages_and_registries/package_registry/mock_data'; import component from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue'; import { + FETCH_PACKAGE_METADATA_ERROR_MESSAGE, PACKAGE_TYPE_NUGET, PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN, @@ -16,6 +23,9 @@ import { PACKAGE_TYPE_COMPOSER, PACKAGE_TYPE_PYPI, } from '~/packages_and_registries/package_registry/constants'; +import AdditionalMetadataLoader from '~/packages_and_registries/package_registry/components/details/additional_metadata_loader.vue'; +import waitForPromises from 'helpers/wait_for_promises'; +import getPackageMetadata from '~/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql'; const mavenPackage = { packageType: PACKAGE_TYPE_MAVEN, metadata: mavenMetadata() }; const conanPackage = { packageType: PACKAGE_TYPE_CONAN, metadata: conanMetadata() }; @@ -24,16 +34,26 @@ const composerPackage = { packageType: PACKAGE_TYPE_COMPOSER, metadata: composer const pypiPackage = { packageType: PACKAGE_TYPE_PYPI, metadata: pypiMetadata() }; const npmPackage = { packageType: PACKAGE_TYPE_NPM, metadata: {} }; -describe('Package Additional Metadata', () => { +Vue.use(VueApollo); + +describe('Package Additional metadata', () => { let wrapper; + let apolloProvider; + const defaultProps = { - packageEntity: { - ...packageData(mavenPackage), - }, + packageId: packageData().id, + packageType: PACKAGE_TYPE_MAVEN, }; - const mountComponent = (props) => { + const mountComponent = ({ + props = {}, + resolver = jest.fn().mockResolvedValue(packageMetadataQuery(mavenPackage)), + } = {}) => { + const requestHandlers = [[getPackageMetadata, resolver]]; + apolloProvider = createMockApollo(requestHandlers); + wrapper = shallowMountExtended(component, { + apolloProvider, propsData: { ...defaultProps, ...props }, stubs: { component: { template: '<div data-testid="component-is"></div>' }, @@ -41,6 +61,10 @@ describe('Package Additional Metadata', () => { }); }; + beforeEach(() => { + jest.spyOn(Sentry, 'captureException').mockImplementation(); + }); + afterEach(() => { wrapper.destroy(); wrapper = null; @@ -49,6 +73,22 @@ describe('Package Additional Metadata', () => { const findTitle = () => wrapper.findByTestId('title'); const findMainArea = () => wrapper.findByTestId('main'); const findComponentIs = () => wrapper.findByTestId('component-is'); + const findAdditionalMetadataLoader = () => wrapper.findComponent(AdditionalMetadataLoader); + const findPackageMetadataAlert = () => wrapper.findComponent(GlAlert); + + it('renders the loading container when loading', () => { + mountComponent(); + + expect(findAdditionalMetadataLoader().exists()).toBe(true); + }); + + it('does not render the loading container once resolved', async () => { + mountComponent(); + await waitForPromises(); + + expect(findAdditionalMetadataLoader().exists()).toBe(false); + expect(Sentry.captureException).not.toHaveBeenCalled(); + }); it('has the correct title', () => { mountComponent(); @@ -56,7 +96,25 @@ describe('Package Additional Metadata', () => { const title = findTitle(); expect(title.exists()).toBe(true); - expect(title.text()).toBe('Additional Metadata'); + expect(title.text()).toMatchInterpolatedText(component.i18n.componentTitle); + }); + + it('does not render gl-alert', () => { + mountComponent(); + + expect(findPackageMetadataAlert().exists()).toBe(false); + }); + + it('renders gl-alert if load fails', async () => { + mountComponent({ resolver: jest.fn().mockRejectedValue() }); + + await waitForPromises(); + + expect(findPackageMetadataAlert().exists()).toBe(true); + expect(findPackageMetadataAlert().text()).toMatchInterpolatedText( + FETCH_PACKAGE_METADATA_ERROR_MESSAGE, + ); + expect(Sentry.captureException).toHaveBeenCalled(); }); it.each` @@ -68,16 +126,22 @@ describe('Package Additional Metadata', () => { ${pypiPackage} | ${true} | ${PACKAGE_TYPE_PYPI} ${npmPackage} | ${false} | ${PACKAGE_TYPE_NPM} `( - `It is $visible that the component is visible when the package is $packageType`, - ({ packageEntity, visible }) => { - mountComponent({ packageEntity }); + `component visibility is $visible when the package is $packageType`, + async ({ packageEntity, visible, packageType }) => { + const resolved = packageMetadataQuery(packageType); + const resolver = jest.fn().mockResolvedValue(resolved); + + mountComponent({ props: { packageType }, resolver }); + + await waitForPromises(); + await nextTick(); expect(findTitle().exists()).toBe(visible); expect(findMainArea().exists()).toBe(visible); expect(findComponentIs().exists()).toBe(visible); if (visible) { - expect(findComponentIs().props('packageEntity')).toEqual(packageEntity); + expect(findComponentIs().props('packageMetadata')).toEqual(packageEntity.metadata); } }, ); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js index e744680cb9a..bb6846d354f 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js @@ -1,22 +1,16 @@ import { GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { - packageData, - composerMetadata, -} from 'jest/packages_and_registries/package_registry/mock_data'; +import { composerMetadata } from 'jest/packages_and_registries/package_registry/mock_data'; import component from '~/packages_and_registries/package_registry/components/details/metadata/composer.vue'; -import { PACKAGE_TYPE_COMPOSER } from '~/packages_and_registries/package_registry/constants'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; -const composerPackage = { packageType: PACKAGE_TYPE_COMPOSER, metadata: composerMetadata() }; - describe('Composer Metadata', () => { let wrapper; const mountComponent = () => { wrapper = shallowMountExtended(component, { - propsData: { packageEntity: packageData(composerPackage) }, + propsData: { packageMetadata: composerMetadata() }, stubs: { DetailsRow, GlSprintf, diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js index 46593047f1f..e7e47401aa1 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js @@ -1,22 +1,16 @@ import { GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { - conanMetadata, - packageData, -} from 'jest/packages_and_registries/package_registry/mock_data'; +import { conanMetadata } from 'jest/packages_and_registries/package_registry/mock_data'; import component from '~/packages_and_registries/package_registry/components/details/metadata/conan.vue'; -import { PACKAGE_TYPE_CONAN } from '~/packages_and_registries/package_registry/constants'; import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; -const conanPackage = { packageType: PACKAGE_TYPE_CONAN, metadata: conanMetadata() }; - describe('Conan Metadata', () => { let wrapper; const mountComponent = () => { wrapper = shallowMountExtended(component, { propsData: { - packageEntity: packageData(conanPackage), + packageMetadata: conanMetadata(), }, stubs: { DetailsRow, diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js index bc54cf1cb98..8680d983042 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js @@ -1,24 +1,16 @@ import { GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { - mavenMetadata, - packageData, -} from 'jest/packages_and_registries/package_registry/mock_data'; +import { mavenMetadata } from 'jest/packages_and_registries/package_registry/mock_data'; import component from '~/packages_and_registries/package_registry/components/details/metadata/maven.vue'; -import { PACKAGE_TYPE_MAVEN } from '~/packages_and_registries/package_registry/constants'; import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; -const mavenPackage = { packageType: PACKAGE_TYPE_MAVEN, metadata: mavenMetadata() }; - describe('Maven Metadata', () => { let wrapper; const mountComponent = () => { wrapper = shallowMountExtended(component, { propsData: { - packageEntity: { - ...packageData(mavenPackage), - }, + packageMetadata: mavenMetadata(), }, stubs: { DetailsRow, diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js index f759fe7a81c..af3692023f0 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js @@ -1,25 +1,17 @@ import { GlLink, GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { - nugetMetadata, - packageData, -} from 'jest/packages_and_registries/package_registry/mock_data'; +import { nugetMetadata } from 'jest/packages_and_registries/package_registry/mock_data'; import component from '~/packages_and_registries/package_registry/components/details/metadata/nuget.vue'; -import { PACKAGE_TYPE_NUGET } from '~/packages_and_registries/package_registry/constants'; import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; describe('Nuget Metadata', () => { - let nugetPackage = { packageType: PACKAGE_TYPE_NUGET, metadata: nugetMetadata() }; + let nugetPackageMetadata = { ...nugetMetadata() }; let wrapper; - const mountComponent = () => { + const mountComponent = (props) => { wrapper = shallowMountExtended(component, { - propsData: { - packageEntity: { - ...packageData(nugetPackage), - }, - }, + propsData: { ...props }, stubs: { DetailsRow, GlSprintf, @@ -37,7 +29,7 @@ describe('Nuget Metadata', () => { const findElementLink = (container) => container.findComponent(GlLink); beforeEach(() => { - mountComponent({ packageEntity: nugetPackage }); + mountComponent({ packageMetadata: nugetPackageMetadata }); }); it.each` @@ -49,14 +41,14 @@ describe('Nuget Metadata', () => { expect(element.exists()).toBe(true); expect(element.text()).toBe(text); expect(element.props('icon')).toBe(icon); - expect(findElementLink(element).attributes('href')).toBe(nugetPackage.metadata[link]); + expect(findElementLink(element).attributes('href')).toBe(nugetPackageMetadata[link]); }); describe('without source', () => { beforeAll(() => { - nugetPackage = { - packageType: PACKAGE_TYPE_NUGET, - metadata: { iconUrl: 'iconUrl', licenseUrl: 'licenseUrl' }, + nugetPackageMetadata = { + iconUrl: 'iconUrl', + licenseUrl: 'licenseUrl', }; }); @@ -67,9 +59,9 @@ describe('Nuget Metadata', () => { describe('without license', () => { beforeAll(() => { - nugetPackage = { - packageType: PACKAGE_TYPE_NUGET, - metadata: { iconUrl: 'iconUrl', projectUrl: 'projectUrl' }, + nugetPackageMetadata = { + iconUrl: 'iconUrl', + projectUrl: 'projectUrl', }; }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js index c4481c3f20b..d7c6ea8379d 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js @@ -1,22 +1,17 @@ import { GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { packageData, pypiMetadata } from 'jest/packages_and_registries/package_registry/mock_data'; +import { pypiMetadata } from 'jest/packages_and_registries/package_registry/mock_data'; import component from '~/packages_and_registries/package_registry/components/details/metadata/pypi.vue'; -import { PACKAGE_TYPE_PYPI } from '~/packages_and_registries/package_registry/constants'; import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; -const pypiPackage = { packageType: PACKAGE_TYPE_PYPI, metadata: pypiMetadata() }; - describe('Package Additional Metadata', () => { let wrapper; const mountComponent = () => { wrapper = shallowMountExtended(component, { propsData: { - packageEntity: { - ...packageData(pypiPackage), - }, + packageMetadata: pypiMetadata(), }, stubs: { DetailsRow, diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js index e68916ecb39..f4e6d43812d 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { GlLink, GlSprintf } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; import { stubComponent } from 'helpers/stub_component'; import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; @@ -11,7 +11,6 @@ import { packagePipelinesQuery, } from 'jest/packages_and_registries/package_registry/mock_data'; import { HISTORY_PIPELINES_LIMIT } from '~/packages_and_registries/shared/constants'; -import { FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE } from '~/packages_and_registries/package_registry/constants'; import component from '~/packages_and_registries/package_registry/components/details/package_history.vue'; import PackageHistoryLoader from '~/packages_and_registries/package_registry/components/details/package_history_loader.vue'; import HistoryItem from '~/vue_shared/components/registry/history_item.vue'; @@ -19,7 +18,8 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import waitForPromises from 'helpers/wait_for_promises'; import getPackagePipelines from '~/packages_and_registries/package_registry/graphql/queries/get_package_pipelines.query.graphql'; -jest.mock('~/flash'); +Vue.use(VueApollo); + describe('Package History', () => { let wrapper; let apolloProvider; @@ -34,12 +34,10 @@ describe('Package History', () => { const createPipelines = (amount) => [...Array(amount)].map((x, index) => packagePipelines({ id: index + 1 })[0]); - const mountComponent = ( - props, + const mountComponent = ({ + props = {}, resolver = jest.fn().mockResolvedValue(packagePipelinesQuery()), - ) => { - Vue.use(VueApollo); - + } = {}) => { const requestHandlers = [[getPackagePipelines, resolver]]; apolloProvider = createMockApollo(requestHandlers); @@ -55,14 +53,20 @@ describe('Package History', () => { }); }; + beforeEach(() => { + jest.spyOn(Sentry, 'captureException').mockImplementation(); + }); + afterEach(() => { wrapper.destroy(); + wrapper = null; }); const findPackageHistoryLoader = () => wrapper.findComponent(PackageHistoryLoader); const findHistoryElement = (testId) => wrapper.findByTestId(testId); const findElementLink = (container) => container.findComponent(GlLink); const findElementTimeAgo = (container) => container.findComponent(TimeAgoTooltip); + const findPackageHistoryAlert = () => wrapper.findComponent(GlAlert); const findTitle = () => wrapper.findByTestId('title'); const findTimeline = () => wrapper.findByTestId('timeline'); @@ -77,6 +81,7 @@ describe('Package History', () => { await waitForPromises(); expect(findPackageHistoryLoader().exists()).toBe(false); + expect(Sentry.captureException).not.toHaveBeenCalled(); }); it('has the correct title', async () => { @@ -101,16 +106,22 @@ describe('Package History', () => { ); }); - it('calls createFlash function if load fails', async () => { - mountComponent({}, jest.fn().mockRejectedValue()); + it('does not render gl-alert', () => { + mountComponent(); + + expect(findPackageHistoryAlert().exists()).toBe(false); + }); + + it('renders gl-alert if load fails', async () => { + mountComponent({ resolver: jest.fn().mockRejectedValue() }); await waitForPromises(); - expect(createFlash).toHaveBeenCalledWith( - expect.objectContaining({ - message: FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE, - }), + expect(findPackageHistoryAlert().exists()).toBe(true); + expect(findPackageHistoryAlert().text()).toEqual( + 'Something went wrong while fetching the package history.', ); + expect(Sentry.captureException).toHaveBeenCalled(); }); describe.each` @@ -132,13 +143,16 @@ describe('Package History', () => { const pipelinesResolver = jest .fn() .mockResolvedValue(packagePipelinesQuery(createPipelines(amount))); - mountComponent( - { + + mountComponent({ + props: { packageEntity, }, - pipelinesResolver, - ); + resolver: pipelinesResolver, + }); + await waitForPromises(); + element = findHistoryElement(name); }); diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js index 3dfcec37ea7..d40feee582f 100644 --- a/spec/frontend/packages_and_registries/package_registry/mock_data.js +++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js @@ -148,6 +148,8 @@ export const conanMetadata = () => ({ recipePath: 'package-8/1.0.0/gitlab-org+gitlab-test/stable', }); +const conanMetadataQuery = () => ({ ...conanMetadata(), __typename: 'ConanMetadata' }); + export const composerMetadata = () => ({ targetSha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0', composerJson: { @@ -156,23 +158,45 @@ export const composerMetadata = () => ({ }, }); +const composerMetadataQuery = () => ({ + ...composerMetadata(), + __typename: 'ComposerMetadata', +}); + export const pypiMetadata = () => ({ + id: 'pypi-1', requiredPython: '1.0.0', }); +const pypiMetadataQuery = () => ({ ...pypiMetadata(), __typename: 'PypiMetadata' }); + export const mavenMetadata = () => ({ + id: 'maven-1', appName: 'appName', appGroup: 'appGroup', appVersion: 'appVersion', path: 'path', }); +const mavenMetadataQuery = () => ({ ...mavenMetadata(), __typename: 'MavenMetadata' }); + export const nugetMetadata = () => ({ + id: 'nuget-1', iconUrl: 'iconUrl', licenseUrl: 'licenseUrl', projectUrl: 'projectUrl', }); +const nugetMetadataQuery = () => ({ ...nugetMetadata(), __typename: 'NugetMetadata' }); + +const packageTypeMetadataQueryMapping = { + CONAN: conanMetadataQuery, + COMPOSER: composerMetadataQuery, + PYPI: pypiMetadataQuery, + MAVEN: mavenMetadataQuery, + NUGET: nugetMetadataQuery, +}; + export const pagination = (extend) => ({ endCursor: 'eyJpZCI6IjIwNSIsIm5hbWUiOiJteS9jb21wYW55L2FwcC9teS1hcHAifQ', hasNextPage: true, @@ -202,6 +226,10 @@ export const packageDetailsQuery = (extendPackage) => ({ nodes: packageTags(), __typename: 'PackageTagConnection', }, + pipelines: { + nodes: packagePipelines(), + __typename: 'PipelineConnection', + }, packageFiles: { nodes: packageFiles(), __typename: 'PackageFileConnection', @@ -240,6 +268,21 @@ export const emptyPackageDetailsQuery = () => ({ }, }); +export const packageMetadataQuery = (packageType) => { + return { + data: { + package: { + id: 'gid://gitlab/Packages::Package/111', + packageType, + metadata: { + ...(packageTypeMetadataQueryMapping[packageType]?.() ?? {}), + }, + __typename: 'PackageDetailsType', + }, + }, + }; +}; + export const packageDestroyMutation = () => ({ data: { destroyPackage: { diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js index a7e31d42c9e..3cadb001c58 100644 --- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js @@ -23,6 +23,10 @@ import { DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, DELETE_PACKAGE_FILE_ERROR_MESSAGE, PACKAGE_TYPE_NUGET, + PACKAGE_TYPE_MAVEN, + PACKAGE_TYPE_CONAN, + PACKAGE_TYPE_PYPI, + PACKAGE_TYPE_NPM, } from '~/packages_and_registries/package_registry/constants'; import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql'; @@ -160,15 +164,38 @@ describe('PackagesApp', () => { }); }); - it('renders additional metadata and has the right props', async () => { - createComponent(); + describe('additional metadata', () => { + it.each` + packageType | visible + ${PACKAGE_TYPE_MAVEN} | ${true} + ${PACKAGE_TYPE_CONAN} | ${true} + ${PACKAGE_TYPE_NUGET} | ${true} + ${PACKAGE_TYPE_COMPOSER} | ${true} + ${PACKAGE_TYPE_PYPI} | ${true} + ${PACKAGE_TYPE_NPM} | ${false} + `( + `It is $visible that the component is visible when the package is $packageType`, + async ({ packageType, visible }) => { + createComponent({ + resolver: jest.fn().mockResolvedValue( + packageDetailsQuery({ + packageType, + }), + ), + }); - await waitForPromises(); + await waitForPromises(); - expect(findAdditionalMetadata().exists()).toBe(true); - expect(findAdditionalMetadata().props()).toMatchObject({ - packageEntity: expect.objectContaining(packageWithoutTypename), - }); + expect(findAdditionalMetadata().exists()).toBe(visible); + + if (visible) { + expect(findAdditionalMetadata().props()).toMatchObject({ + packageId: packageWithoutTypename.id, + packageType, + }); + } + }, + ); }); it('renders installation commands and has the right props', async () => { diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js index 0d85df25b4f..2c3f6ef8634 100644 --- a/spec/frontend/work_items/components/item_title_spec.js +++ b/spec/frontend/work_items/components/item_title_spec.js @@ -15,7 +15,7 @@ const createComponent = ({ title = 'Sample title', disabled = false } = {}) => describe('ItemTitle', () => { let wrapper; const mockUpdatedTitle = 'Updated title'; - const findInputEl = () => wrapper.find('span#item-title'); + const findInputEl = () => wrapper.find('[aria-label="Title"]'); beforeEach(() => { wrapper = createComponent(); diff --git a/spec/graphql/mutations/incident_management/timeline_event/create_spec.rb b/spec/graphql/mutations/incident_management/timeline_event/create_spec.rb index 63faecad5d5..ea74e427dd6 100644 --- a/spec/graphql/mutations/incident_management/timeline_event/create_spec.rb +++ b/spec/graphql/mutations/incident_management/timeline_event/create_spec.rb @@ -22,7 +22,8 @@ RSpec.describe Mutations::IncidentManagement::TimelineEvent::Create do occurred_at: args[:occurred_at].to_s, incident: incident, author: current_user, - promoted_from_note: nil + promoted_from_note: nil, + editable: true ) end diff --git a/spec/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb b/spec/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb index 598ee496cf1..4541f8af7d3 100644 --- a/spec/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb +++ b/spec/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb @@ -27,7 +27,8 @@ RSpec.describe Mutations::IncidentManagement::TimelineEvent::PromoteFromNote do occurred_at: comment.created_at.to_s, incident: incident, author: current_user, - promoted_from_note: comment + promoted_from_note: comment, + editable: true ) end diff --git a/spec/graphql/resolvers/crm/contacts_resolver_spec.rb b/spec/graphql/resolvers/crm/contacts_resolver_spec.rb index eba26c8c71f..98da4aeac28 100644 --- a/spec/graphql/resolvers/crm/contacts_resolver_spec.rb +++ b/spec/graphql/resolvers/crm/contacts_resolver_spec.rb @@ -41,7 +41,7 @@ RSpec.describe Resolvers::Crm::ContactsResolver do end context 'with authorized user' do - it 'does not rise an error and returns all contacts' do + it 'does not rise an error and returns all contacts in the correct order' do group.add_reporter(user) expect { resolve_contacts(group) }.not_to raise_error @@ -61,20 +61,26 @@ RSpec.describe Resolvers::Crm::ContactsResolver do end context 'when no filter is provided' do - it 'returns all the contacts' do - expect(resolve_contacts(group)).to match_array([contact_a, contact_b]) + it 'returns all the contacts in the correct order' do + expect(resolve_contacts(group)).to eq([contact_a, contact_b]) end end context 'when search term is provided' do it 'returns the correct contacts' do - expect(resolve_contacts(group, { search: "x@test.com" })).to match_array([contact_b]) + expect(resolve_contacts(group, { search: "x@test.com" })).to contain_exactly(contact_b) end end context 'when state is provided' do it 'returns the correct contacts' do - expect(resolve_contacts(group, { state: :inactive })).to match_array([contact_a]) + expect(resolve_contacts(group, { state: :inactive })).to contain_exactly(contact_a) + end + end + + context 'when ids are provided' do + it 'returns the correct contacts' do + expect(resolve_contacts(group, { ids: [contact_a.to_global_id] })).to contain_exactly(contact_a) end end end diff --git a/spec/graphql/resolvers/crm/organizations_resolver_spec.rb b/spec/graphql/resolvers/crm/organizations_resolver_spec.rb index c80caf91f90..323f134ffc3 100644 --- a/spec/graphql/resolvers/crm/organizations_resolver_spec.rb +++ b/spec/graphql/resolvers/crm/organizations_resolver_spec.rb @@ -35,7 +35,7 @@ RSpec.describe Resolvers::Crm::OrganizationsResolver do end context 'with authorized user' do - it 'does not rise an error and returns all organizations' do + it 'does not rise an error and returns all organizations in the correct order' do group.add_reporter(user) expect { resolve_organizations(group) }.not_to raise_error @@ -55,20 +55,28 @@ RSpec.describe Resolvers::Crm::OrganizationsResolver do end context 'when no filter is provided' do - it 'returns all the organizations' do - expect(resolve_organizations(group)).to match_array([organization_a, organization_b]) + it 'returns all the organizations in the correct order' do + expect(resolve_organizations(group)).to eq([organization_a, organization_b]) end end context 'when search term is provided' do it 'returns the correct organizations' do - expect(resolve_organizations(group, { search: "def" })).to match_array([organization_b]) + expect(resolve_organizations(group, { search: "def" })).to contain_exactly(organization_b) end end context 'when state is provided' do it 'returns the correct organizations' do - expect(resolve_organizations(group, { state: :inactive })).to match_array([organization_a]) + expect(resolve_organizations(group, { state: :inactive })).to contain_exactly(organization_a) + end + end + + context 'when ids are provided' do + it 'returns the correct organizations' do + expect(resolve_organizations(group, { + ids: [organization_b.to_global_id] + })).to contain_exactly(organization_b) end end end diff --git a/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb index 85c25938fcc..2633598b48d 100644 --- a/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb +++ b/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb @@ -31,7 +31,8 @@ RSpec.describe BulkImports::Projects::Pipelines::ReleasesPipeline do 'created_at' => '2019-12-26T10:17:14.621Z', 'updated_at' => '2019-12-26T10:17:14.621Z', 'released_at' => '2019-12-26T10:17:14.615Z', - 'sha' => '901de3a8bd5573f4a049b1457d28bc1592ba6bf9' + 'sha' => '901de3a8bd5573f4a049b1457d28bc1592ba6bf9', + 'author_id' => user.id }.merge(attributes) end @@ -62,6 +63,7 @@ RSpec.describe BulkImports::Projects::Pipelines::ReleasesPipeline do expect(imported_release.updated_at.to_s).to eq('2019-12-26 10:17:14 UTC') expect(imported_release.released_at.to_s).to eq('2019-12-26 10:17:14 UTC') expect(imported_release.sha).to eq(release['sha']) + expect(imported_release.author_id).to eq(release['author_id']) end end diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb index f81ad9b193d..8cdfa28d40a 100644 --- a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb +++ b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb @@ -77,8 +77,8 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do context 'for redis metrics' do it_behaves_like 'name suggestion' do - # corresponding metric is collected with redis_usage_data { unique_visit_service.unique_visits_for(targets: :analytics) } - let(:key_path) { 'analytics_unique_visits.analytics_unique_visits_for_any_target' } + # corresponding metric is collected with redis_usage_data { unique_visit_service.unique_visits_for(targets: :compliance) } + let(:key_path) { 'compliance_unique_visits.compliance_unique_visits_for_any_target' } let(:name_suggestion) { /<please fill metric name, suggested format is: {subject}_{verb}{ing|ed}_{object} eg: users_creating_epics or merge_requests_viewed_in_single_file_mode>/ } end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 9d7f464756d..a0a1a216cb6 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -1204,46 +1204,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do end end - describe '.analytics_unique_visits_data' do - subject { described_class.analytics_unique_visits_data } - - it 'returns the number of unique visits to pages with analytics features' do - ::Gitlab::Analytics::UniqueVisits.analytics_events.each do |target| - expect_any_instance_of(::Gitlab::Analytics::UniqueVisits).to receive(:unique_visits_for).with(targets: target).and_return(123) - end - - expect_any_instance_of(::Gitlab::Analytics::UniqueVisits).to receive(:unique_visits_for).with(targets: :analytics).and_return(543) - expect_any_instance_of(::Gitlab::Analytics::UniqueVisits).to receive(:unique_visits_for).with(targets: :analytics, start_date: 4.weeks.ago.to_date, end_date: Date.current).and_return(987) - - expect(subject).to eq({ - analytics_unique_visits: { - 'g_analytics_contribution' => 123, - 'g_analytics_insights' => 123, - 'g_analytics_issues' => 123, - 'g_analytics_productivity' => 123, - 'g_analytics_valuestream' => 123, - 'p_analytics_pipelines' => 123, - 'p_analytics_code_reviews' => 123, - 'p_analytics_valuestream' => 123, - 'p_analytics_insights' => 123, - 'p_analytics_issues' => 123, - 'p_analytics_repo' => 123, - 'i_analytics_cohorts' => 123, - 'i_analytics_dev_ops_score' => 123, - 'i_analytics_instance_statistics' => 123, - 'p_analytics_ci_cd_deployment_frequency' => 123, - 'p_analytics_ci_cd_lead_time' => 123, - 'p_analytics_ci_cd_pipelines' => 123, - 'p_analytics_merge_request' => 123, - 'i_analytics_dev_ops_adoption' => 123, - 'users_viewing_analytics_group_devops_adoption' => 123, - 'analytics_unique_visits_for_any_target' => 543, - 'analytics_unique_visits_for_any_target_monthly' => 987 - } - }) - end - end - describe '.compliance_unique_visits_data' do subject { described_class.compliance_unique_visits_data } diff --git a/spec/models/customer_relations/contact_spec.rb b/spec/models/customer_relations/contact_spec.rb index 86f868b269e..f91546f5240 100644 --- a/spec/models/customer_relations/contact_spec.rb +++ b/spec/models/customer_relations/contact_spec.rb @@ -142,4 +142,99 @@ RSpec.describe CustomerRelations::Contact, type: :model do expect(issue_contact2.reload.contact_id).to eq(dupe_contact1.id) end end + + describe '.search' do + let_it_be(:contact_a) do + create( + :contact, + group: group, + first_name: "ABC", + last_name: "DEF", + email: "ghi@test.com", + description: "LMNO", + state: "inactive" + ) + end + + let_it_be(:contact_b) do + create( + :contact, + group: group, + first_name: "PQR", + last_name: "STU", + email: "vwx@test.com", + description: "YZ", + state: "active" + ) + end + + subject(:found_contacts) { group.contacts.search(search_term) } + + context 'when search term is empty' do + let(:search_term) { "" } + + it 'returns all group contacts' do + expect(found_contacts).to contain_exactly(contact_a, contact_b) + end + end + + context 'when search term is not empty' do + context 'when searching for first name ignoring casing' do + let(:search_term) { "aBc" } + + it { is_expected.to contain_exactly(contact_a) } + end + + context 'when searching for last name ignoring casing' do + let(:search_term) { "StU" } + + it { is_expected.to contain_exactly(contact_b) } + end + + context 'when searching for email' do + let(:search_term) { "ghi" } + + it { is_expected.to contain_exactly(contact_a) } + end + + context 'when searching description ignoring casing' do + let(:search_term) { "Yz" } + + it { is_expected.to contain_exactly(contact_b) } + end + + context 'when fuzzy searching for email and last name' do + let(:search_term) { "s" } + + it { is_expected.to contain_exactly(contact_a, contact_b) } + end + end + end + + describe '.search_by_state' do + let_it_be(:contact_a) { create(:contact, group: group, state: "inactive") } + let_it_be(:contact_b) { create(:contact, group: group, state: "active") } + + context 'when searching for contacts state' do + it 'returns only inactive contacts' do + expect(group.contacts.search_by_state(:inactive)).to contain_exactly(contact_a) + end + + it 'returns only active contacts' do + expect(group.contacts.search_by_state(:active)).to contain_exactly(contact_b) + end + end + end + + describe '.sort_by_name' do + let_it_be(:contact_a) { create(:contact, group: group, first_name: "c", last_name: "d") } + let_it_be(:contact_b) { create(:contact, group: group, first_name: "a", last_name: "b") } + let_it_be(:contact_c) { create(:contact, group: group, first_name: "e", last_name: "d") } + + context 'when sorting the contacts' do + it 'sorts them by last name then first name in ascendent order' do + expect(group.contacts.sort_by_name).to eq([contact_b, contact_a, contact_c]) + end + end + end end diff --git a/spec/models/customer_relations/organization_spec.rb b/spec/models/customer_relations/organization_spec.rb index 06ba9c5b7ad..1833fcf5385 100644 --- a/spec/models/customer_relations/organization_spec.rb +++ b/spec/models/customer_relations/organization_spec.rb @@ -78,4 +78,83 @@ RSpec.describe CustomerRelations::Organization, type: :model do expect(contact2.reload.organization_id).to eq(dupe_organization1.id) end end + + describe '.search' do + let_it_be(:organization_a) do + create( + :organization, + group: group, + name: "DEF", + description: "ghi_st", + state: "inactive" + ) + end + + let_it_be(:organization_b) do + create( + :organization, + group: group, + name: "ABC_st", + description: "JKL", + state: "active" + ) + end + + subject(:found_organizations) { group.organizations.search(search_term) } + + context 'when search term is empty' do + let(:search_term) { "" } + + it 'returns all group organizations' do + expect(found_organizations).to contain_exactly(organization_a, organization_b) + end + end + + context 'when search term is not empty' do + context 'when searching for name' do + let(:search_term) { "aBc" } + + it { is_expected.to contain_exactly(organization_b) } + end + + context 'when searching for description' do + let(:search_term) { "ghI" } + + it { is_expected.to contain_exactly(organization_a) } + end + + context 'when searching for name and description' do + let(:search_term) { "_st" } + + it { is_expected.to contain_exactly(organization_a, organization_b) } + end + end + end + + describe '.search_by_state' do + let_it_be(:organization_a) { create(:organization, group: group, state: "inactive") } + let_it_be(:organization_b) { create(:organization, group: group, state: "active") } + + context 'when searching for organizations state' do + it 'returns only inactive organizations' do + expect(group.organizations.search_by_state(:inactive)).to contain_exactly(organization_a) + end + + it 'returns only active organizations' do + expect(group.organizations.search_by_state(:active)).to contain_exactly(organization_b) + end + end + end + + describe '.sort_by_name' do + let_it_be(:organization_a) { create(:organization, group: group, name: "c") } + let_it_be(:organization_b) { create(:organization, group: group, name: "a") } + let_it_be(:organization_c) { create(:organization, group: group, name: "b") } + + context 'when sorting the organizations' do + it 'sorts them by name in ascendent order' do + expect(group.organizations.sort_by_name).to eq([organization_b, organization_c, organization_a]) + end + end + end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 06044cf53cc..72a57b6076a 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -82,14 +82,16 @@ RSpec.describe Milestone do context 'when it is tied to a release for another project' do it 'creates a validation error' do other_project = create(:project) - milestone.releases << build(:release, project: other_project) + milestone.releases << build(:release, + project: other_project, author_id: other_project.members.first.user_id) expect(milestone).not_to be_valid end end context 'when it is tied to a release for the same project' do it 'is valid' do - milestone.releases << build(:release, project: project) + milestone.releases << build(:release, + project: project, author_id: project.members.first.user_id) expect(milestone).to be_valid end end diff --git a/spec/models/projects/build_artifacts_size_refresh_spec.rb b/spec/models/projects/build_artifacts_size_refresh_spec.rb index f8cd8fb5c76..052e654af76 100644 --- a/spec/models/projects/build_artifacts_size_refresh_spec.rb +++ b/spec/models/projects/build_artifacts_size_refresh_spec.rb @@ -30,6 +30,12 @@ RSpec.describe Projects::BuildArtifactsSizeRefresh, type: :model do expect(described_class.remaining).to match_array([refresh_1, refresh_3, refresh_4]) end end + + describe 'processing_queue' do + it 'prioritizes pending -> stale -> created' do + expect(described_class.processing_queue).to eq([refresh_3, refresh_1, refresh_4]) + end + end end describe 'state machine', :clean_gitlab_redis_shared_state do @@ -165,15 +171,13 @@ RSpec.describe Projects::BuildArtifactsSizeRefresh, type: :model do end describe '.process_next_refresh!' do - let!(:refresh_running) { create(:project_build_artifacts_size_refresh, :running) } let!(:refresh_created) { create(:project_build_artifacts_size_refresh, :created) } - let!(:refresh_stale) { create(:project_build_artifacts_size_refresh, :stale) } let!(:refresh_pending) { create(:project_build_artifacts_size_refresh, :pending) } subject(:processed_refresh) { described_class.process_next_refresh! } it 'picks the first record from the remaining work' do - expect(processed_refresh).to eq(refresh_created) + expect(processed_refresh).to eq(refresh_pending) expect(processed_refresh.reload).to be_running end end diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb index 4ae1927dcca..83d7596ff51 100644 --- a/spec/models/release_spec.rb +++ b/spec/models/release_spec.rb @@ -66,6 +66,32 @@ RSpec.describe Release do expect { release.milestones << milestone }.to change { MilestoneRelease.count }.by(1) end end + + context 'when creating new release' do + subject { build(:release, project: project, name: 'Release 1.0') } + + it { is_expected.to validate_presence_of(:author_id) } + + context 'when feature flag is disabled' do + before do + stub_feature_flags(validate_release_with_author: false) + end + + it { is_expected.not_to validate_presence_of(:author_id) } + end + end + + # Mimic releases created before 11.7 + # See: https://gitlab.com/gitlab-org/gitlab/-/blob/8e5a110b01f842d8b6a702197928757a40ce9009/app/models/release.rb#L14 + context 'when updating existing release without author' do + let(:release) { create(:release, :legacy) } + + it 'updates successfully' do + release.description += 'Update' + + expect { release.save! }.not_to raise_error + end + end end describe '#assets_count' do diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb index 7ff020f05e8..df3e4b985ab 100644 --- a/spec/presenters/project_presenter_spec.rb +++ b/spec/presenters/project_presenter_spec.rb @@ -73,8 +73,6 @@ RSpec.describe ProjectPresenter do context 'when repository is not empty' do let_it_be(:project) { create(:project, :public, :repository) } - let(:release) { create(:release, project: project, author: user) } - it 'returns files and readme if user has repository access' do allow(presenter).to receive(:can?).with(nil, :download_code, project).and_return(true) @@ -98,6 +96,9 @@ RSpec.describe ProjectPresenter do end it 'returns releases anchor' do + user = create(:user) + release = create(:release, project: project, author: user) + expect(release).to be_truthy expect(presenter.releases_anchor_data).to have_attributes( is_link: true, diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb index 3ea8b38e20f..923e12a3c06 100644 --- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb +++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb @@ -53,7 +53,7 @@ RSpec.describe 'Creating an incident timeline event' do }, 'note' => note, 'action' => 'comment', - 'editable' => false, + 'editable' => true, 'occurredAt' => event_occurred_at.iso8601 ) end diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb index faff3bfe23a..85208869ad9 100644 --- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb @@ -56,7 +56,7 @@ RSpec.describe 'Removing an incident timeline event' do }, 'note' => timeline_event.note, 'noteHtml' => timeline_event.note_html, - 'editable' => false, + 'editable' => true, 'action' => timeline_event.action, 'occurredAt' => timeline_event.occurred_at.iso8601, 'createdAt' => timeline_event.created_at.iso8601, diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb index b92f6af1d3d..9272e218172 100644 --- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb +++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb @@ -55,7 +55,7 @@ RSpec.describe 'Promote an incident timeline event from a comment' do }, 'note' => comment.note, 'action' => 'comment', - 'editable' => false, + 'editable' => true, 'occurredAt' => comment.created_at.iso8601 ) end diff --git a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb index 708fa96986c..31fef75f679 100644 --- a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb +++ b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb @@ -94,7 +94,7 @@ RSpec.describe 'getting incident timeline events' do 'id' => promoted_from_note.to_global_id.to_s, 'body' => promoted_from_note.note }, - 'editable' => false, + 'editable' => true, 'action' => timeline_event.action, 'occurredAt' => timeline_event.occurred_at.iso8601, 'createdAt' => timeline_event.created_at.iso8601, diff --git a/spec/services/incident_management/timeline_events/create_service_spec.rb b/spec/services/incident_management/timeline_events/create_service_spec.rb index 38ce15e74f1..a88a9400832 100644 --- a/spec/services/incident_management/timeline_events/create_service_spec.rb +++ b/spec/services/incident_management/timeline_events/create_service_spec.rb @@ -18,6 +18,7 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do } end + let(:editable) { false } let(:current_user) { user_with_permissions } let(:service) { described_class.new(incident, current_user, args) } @@ -45,6 +46,7 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do expect(result.project).to eq(project) expect(result.note).to eq(args[:note]) expect(result.promoted_from_note).to eq(comment) + expect(result.editable).to eq(editable) end end @@ -90,6 +92,30 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do end end + context 'with editable param' do + let(:args) do + { + note: 'note', + occurred_at: Time.current, + action: 'new comment', + promoted_from_note: comment, + editable: editable + } + end + + context 'when editable is true' do + let(:editable) { true } + + it_behaves_like 'success response' + end + + context 'when editable is false' do + let(:editable) { false } + + it_behaves_like 'success response' + end + end + it 'successfully creates a database record', :aggregate_failures do expect { execute }.to change { ::IncidentManagement::TimelineEvent.count }.by(1) end diff --git a/spec/services/incident_management/timeline_events/update_service_spec.rb b/spec/services/incident_management/timeline_events/update_service_spec.rb index 8bc0e5ce0ed..b1699e993d5 100644 --- a/spec/services/incident_management/timeline_events/update_service_spec.rb +++ b/spec/services/incident_management/timeline_events/update_service_spec.rb @@ -135,6 +135,14 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do execute end end + + context 'when timeline event is non-editable' do + let!(:timeline_event) do + create(:incident_management_timeline_event, :non_editable, project: project, incident: incident) + end + + it_behaves_like 'error response', 'You cannot edit this timeline event.' + end end context 'when user does not have permissions' do diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb index 2fd1b2e9048..591f7973454 100644 --- a/spec/support/shared_examples/features/content_editor_shared_examples.rb +++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb @@ -76,4 +76,49 @@ RSpec.shared_examples 'edits content using the content editor' do expect(find('[data-testid="code-block-bubble-menu"]')).to have_text('Custom (nomnoml)') end end + + describe 'mermaid diagram' do + before do + find(content_editor_testid).send_keys [:enter, :enter] + + find(content_editor_testid).send_keys '```mermaid ' + find(content_editor_testid).send_keys ['graph TD;', :enter, ' JohnDoe12 --> HelloWorld34'] + end + + it 'renders and updates the diagram correctly in a sandboxed iframe' do + iframe = find(content_editor_testid).find('iframe') + expect(iframe['src']).to include('/-/sandbox/mermaid') + + within_frame(iframe) do + expect(find('svg').text).to include('JohnDoe12') + expect(find('svg').text).to include('HelloWorld34') + end + + expect(iframe['height'].to_i).to be > 100 + + find(content_editor_testid).send_keys [:enter, ' JaneDoe34 --> HelloWorld56'] + + within_frame(iframe) do + page.has_content?('JaneDoe34') + + expect(find('svg').text).to include('JaneDoe34') + expect(find('svg').text).to include('HelloWorld56') + end + end + + it 'toggles the diagram when preview button is clicked' do + find('[data-testid="preview-diagram"]').click + + expect(find(content_editor_testid)).not_to have_selector('iframe') + + find('[data-testid="preview-diagram"]').click + + iframe = find(content_editor_testid).find('iframe') + + within_frame(iframe) do + expect(find('svg').text).to include('JohnDoe12') + expect(find('svg').text).to include('HelloWorld34') + end + end + end end diff --git a/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb index b989dbc6524..cd591248ff6 100644 --- a/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb @@ -21,6 +21,7 @@ RSpec.shared_examples 'creating an incident timeline event' do expect(timeline_event.occurred_at.to_s).to eq(expected_timeline_event.occurred_at) expect(timeline_event.incident).to eq(expected_timeline_event.incident) expect(timeline_event.author).to eq(expected_timeline_event.author) + expect(timeline_event.editable).to eq(expected_timeline_event.editable) end end |