Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/factories/incident_management/timeline_events.rb9
-rw-r--r--spec/factories/releases.rb6
-rw-r--r--spec/finders/crm/contacts_finder_spec.rb8
-rw-r--r--spec/finders/crm/organizations_finder_spec.rb8
-rw-r--r--spec/frontend/admin/signup_restrictions/components/signup_form_spec.js7
-rw-r--r--spec/frontend/admin/signup_restrictions/mock_data.js8
-rw-r--r--spec/frontend/admin/signup_restrictions/utils_spec.js4
-rw-r--r--spec/frontend/api_spec.js6
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/code_block_spec.js48
-rw-r--r--spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js54
-rw-r--r--spec/frontend/content_editor/components/top_toolbar_spec.js29
-rw-r--r--spec/frontend/content_editor/components/wrappers/code_block_spec.js83
-rw-r--r--spec/frontend/content_editor/services/asset_resolver_spec.js10
-rw-r--r--spec/frontend/content_editor/services/code_block_language_loader_spec.js7
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js84
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js12
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js32
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js9
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js52
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js43
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js41
-rw-r--r--spec/frontend/work_items/components/item_title_spec.js2
-rw-r--r--spec/graphql/mutations/incident_management/timeline_event/create_spec.rb3
-rw-r--r--spec/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb3
-rw-r--r--spec/graphql/resolvers/crm/contacts_resolver_spec.rb16
-rw-r--r--spec/graphql/resolvers/crm/organizations_resolver_spec.rb18
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb4
-rw-r--r--spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb4
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb40
-rw-r--r--spec/models/customer_relations/contact_spec.rb95
-rw-r--r--spec/models/customer_relations/organization_spec.rb79
-rw-r--r--spec/models/milestone_spec.rb6
-rw-r--r--spec/models/projects/build_artifacts_size_refresh_spec.rb10
-rw-r--r--spec/models/release_spec.rb26
-rw-r--r--spec/presenters/project_presenter_spec.rb5
-rw-r--r--spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb2
-rw-r--r--spec/services/incident_management/timeline_events/create_service_spec.rb26
-rw-r--r--spec/services/incident_management/timeline_events/update_service_spec.rb8
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb45
-rw-r--r--spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb1
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