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:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-10-28 15:11:31 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-10-28 15:11:31 +0300
commit2ebd699ede8f213f6e8f21ba7d1d9904197b2984 (patch)
treeea8a020f8bc1ffce42e95f76629c72c59e94a7be /spec
parent25788905108838d95a62d7e3ad3ca16e6f6d0fda (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js145
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js56
-rw-r--r--spec/frontend/notebook/cells/markdown_spec.js101
-rw-r--r--spec/frontend/notebook/cells/output/index_spec.js14
-rw-r--r--spec/frontend/notebook/cells/output/markdown_spec.js44
-rw-r--r--spec/frontend/notebook/mock_data.js2
-rw-r--r--spec/frontend/releases/components/asset_links_form_spec.js42
-rw-r--r--spec/graphql/graphql_triggers_spec.rb69
-rw-r--r--spec/graphql/types/subscription_type_spec.rb1
-rw-r--r--spec/initializers/hashie_mash_permitted_patch_spec.rb29
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/cookie_spec.rb130
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb164
-rw-r--r--spec/services/issues/update_service_spec.rb29
-rw-r--r--spec/services/work_items/update_service_spec.rb28
14 files changed, 516 insertions, 338 deletions
diff --git a/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js b/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js
index 2b9442162aa..de0e5063e49 100644
--- a/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js
+++ b/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js
@@ -1,34 +1,127 @@
-import $ from 'jquery';
+import { createWrapper } from '@vue/test-utils';
+import { __ } from '~/locale';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import renderMermaid from '~/behaviors/markdown/render_sandboxed_mermaid';
+import renderMermaid, {
+ MAX_CHAR_LIMIT,
+ MAX_MERMAID_BLOCK_LIMIT,
+ LAZY_ALERT_SHOWN_CLASS,
+} from '~/behaviors/markdown/render_sandboxed_mermaid';
-describe('Render mermaid diagrams for Gitlab Flavoured Markdown', () => {
- it('Does something', () => {
- document.body.dataset.page = '';
- setHTMLFixture(`
- <div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="1:1-7:3" class="code highlight js-syntax-highlight language-mermaid white" lang="mermaid" id="code-4">
- <code class="js-render-mermaid">
- <span id="LC1" class="line" lang="mermaid">graph TD;</span>
- <span id="LC2" class="line" lang="mermaid">A--&gt;B</span>
- <span id="LC3" class="line" lang="mermaid">A--&gt;C</span>
- <span id="LC4" class="line" lang="mermaid">B--&gt;D</span>
- <span id="LC5" class="line" lang="mermaid">C--&gt;D</span>
- </code>
- </pre>
- <copy-code>
- <button type="button" class="btn btn-default btn-md gl-button btn-icon has-tooltip" data-title="Copy to clipboard" data-clipboard-target="pre#code-4">
- <svg><use xlink:href="/assets/icons-7f1680a3670112fe4c8ef57b9dfb93f0f61b43a2a479d7abd6c83bcb724b9201.svg#copy-to-clipboard"></use></svg>
- </button>
- </copy-code>
- </div>`);
- const els = $('pre.js-syntax-highlight').find('.js-render-mermaid');
-
- renderMermaid(els);
+describe('Mermaid diagrams renderer', () => {
+ // Finders
+ const findMermaidIframes = () => document.querySelectorAll('iframe[src="/-/sandbox/mermaid"]');
+ const findDangerousMermaidAlert = () =>
+ createWrapper(document.querySelector('[data-testid="alert-warning"]'));
+ // Helpers
+ const renderDiagrams = () => {
+ renderMermaid([...document.querySelectorAll('.js-render-mermaid')]);
jest.runAllTimers();
- expect(document.querySelector('pre.js-syntax-highlight').classList).toContain('gl-sr-only');
+ };
+
+ beforeEach(() => {
+ document.body.dataset.page = '';
+ });
+ afterEach(() => {
resetHTMLFixture();
});
+
+ it('renders a mermaid diagram', () => {
+ setHTMLFixture('<pre><code class="js-render-mermaid"></code></pre>');
+
+ expect(findMermaidIframes()).toHaveLength(0);
+
+ renderDiagrams();
+
+ expect(document.querySelector('pre').classList).toContain('gl-sr-only');
+ expect(findMermaidIframes()).toHaveLength(1);
+ });
+
+ describe('within a details element', () => {
+ beforeEach(() => {
+ setHTMLFixture('<details><pre><code class="js-render-mermaid"></code></pre></details>');
+ renderDiagrams();
+ });
+
+ it('does not render the diagram on load', () => {
+ expect(findMermaidIframes()).toHaveLength(0);
+ });
+
+ it('render the diagram when the details element is opened', () => {
+ document.querySelector('details').setAttribute('open', true);
+ document.querySelector('details').dispatchEvent(new Event('toggle'));
+ jest.runAllTimers();
+
+ expect(findMermaidIframes()).toHaveLength(1);
+ });
+ });
+
+ describe('dangerous diagrams', () => {
+ describe(`when the diagram's source exceeds ${MAX_CHAR_LIMIT} characters`, () => {
+ beforeEach(() => {
+ setHTMLFixture(
+ `<pre>
+ <code class="js-render-mermaid">${Array(MAX_CHAR_LIMIT + 1)
+ .fill('a')
+ .join('')}</code>
+ </pre>`,
+ );
+ renderDiagrams();
+ });
+ it('does not render the diagram on load', () => {
+ expect(findMermaidIframes()).toHaveLength(0);
+ });
+
+ it('shows a warning about performance impact when rendering the diagram', () => {
+ expect(document.querySelector('pre').classList).toContain(LAZY_ALERT_SHOWN_CLASS);
+ expect(findDangerousMermaidAlert().exists()).toBe(true);
+ expect(findDangerousMermaidAlert().text()).toContain(
+ __('Warning: Displaying this diagram might cause performance issues on this page.'),
+ );
+ });
+
+ it("renders the diagram when clicking on the alert's button", () => {
+ findDangerousMermaidAlert().find('button').trigger('click');
+ jest.runAllTimers();
+
+ expect(findMermaidIframes()).toHaveLength(1);
+ });
+ });
+
+ it(`stops rendering diagrams once the total rendered source exceeds ${MAX_CHAR_LIMIT} characters`, () => {
+ setHTMLFixture(
+ `<pre>
+ <code class="js-render-mermaid">${Array(MAX_CHAR_LIMIT - 1)
+ .fill('a')
+ .join('')}</code>
+ <code class="js-render-mermaid">2</code>
+ <code class="js-render-mermaid">3</code>
+ <code class="js-render-mermaid">4</code>
+ </pre>`,
+ );
+ renderDiagrams();
+
+ expect(findMermaidIframes()).toHaveLength(3);
+ });
+
+ // Note: The test case below is provided for convenience but should remain skipped as the DOM
+ // operations it requires are too expensive and would significantly slow down the test suite.
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip(`stops rendering diagrams when the rendered diagrams count exceeds ${MAX_MERMAID_BLOCK_LIMIT}`, () => {
+ setHTMLFixture(
+ `<pre>
+ ${Array(MAX_MERMAID_BLOCK_LIMIT + 1)
+ .fill('<code class="js-render-mermaid"></code>')
+ .join('')}
+ </pre>`,
+ );
+ renderDiagrams();
+
+ expect([...document.querySelectorAll('.js-render-mermaid')]).toHaveLength(
+ MAX_MERMAID_BLOCK_LIMIT + 1,
+ );
+ expect(findMermaidIframes()).toHaveLength(MAX_MERMAID_BLOCK_LIMIT);
+ });
+ });
});
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index 5133c02b190..16631752d6d 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -131,7 +131,6 @@ describe('CE IssuesListApp component', () => {
const mountComponent = ({
provide = {},
data = {},
- workItems = false,
issuesQueryResponse = mockIssuesQueryResponse,
issuesCountsQueryResponse = mockIssuesCountsQueryResponse,
sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
@@ -150,9 +149,6 @@ describe('CE IssuesListApp component', () => {
apolloProvider: createMockApollo(requestHandlers),
router,
provide: {
- glFeatures: {
- workItems,
- },
...defaultProvide,
...provide,
},
@@ -1060,45 +1056,23 @@ describe('CE IssuesListApp component', () => {
});
describe('fetching issues', () => {
- describe('when work_items feature flag is disabled', () => {
- beforeEach(() => {
- wrapper = mountComponent({ workItems: false });
- jest.runOnlyPendingTimers();
- });
-
- it('fetches issue, incident, and test case types', () => {
- const types = [
- WORK_ITEM_TYPE_ENUM_ISSUE,
- WORK_ITEM_TYPE_ENUM_INCIDENT,
- WORK_ITEM_TYPE_ENUM_TEST_CASE,
- ];
-
- expect(mockIssuesQueryResponse).toHaveBeenCalledWith(expect.objectContaining({ types }));
- expect(mockIssuesCountsQueryResponse).toHaveBeenCalledWith(
- expect.objectContaining({ types }),
- );
- });
+ beforeEach(() => {
+ wrapper = mountComponent();
+ jest.runOnlyPendingTimers();
});
- describe('when work_items feature flag is enabled', () => {
- beforeEach(() => {
- wrapper = mountComponent({ workItems: true });
- jest.runOnlyPendingTimers();
- });
-
- it('fetches issue, incident, test case, and task types', () => {
- const types = [
- WORK_ITEM_TYPE_ENUM_ISSUE,
- WORK_ITEM_TYPE_ENUM_INCIDENT,
- WORK_ITEM_TYPE_ENUM_TEST_CASE,
- WORK_ITEM_TYPE_ENUM_TASK,
- ];
-
- expect(mockIssuesQueryResponse).toHaveBeenCalledWith(expect.objectContaining({ types }));
- expect(mockIssuesCountsQueryResponse).toHaveBeenCalledWith(
- expect.objectContaining({ types }),
- );
- });
+ it('fetches issue, incident, test case, and task types', () => {
+ const types = [
+ WORK_ITEM_TYPE_ENUM_ISSUE,
+ WORK_ITEM_TYPE_ENUM_INCIDENT,
+ WORK_ITEM_TYPE_ENUM_TEST_CASE,
+ WORK_ITEM_TYPE_ENUM_TASK,
+ ];
+
+ expect(mockIssuesQueryResponse).toHaveBeenCalledWith(expect.objectContaining({ types }));
+ expect(mockIssuesCountsQueryResponse).toHaveBeenCalledWith(
+ expect.objectContaining({ types }),
+ );
});
});
});
diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js
index c757b55faf4..a7776bd5b69 100644
--- a/spec/frontend/notebook/cells/markdown_spec.js
+++ b/spec/frontend/notebook/cells/markdown_spec.js
@@ -5,20 +5,22 @@ import markdownTableJson from 'test_fixtures/blob/notebook/markdown-table.json';
import basicJson from 'test_fixtures/blob/notebook/basic.json';
import mathJson from 'test_fixtures/blob/notebook/math.json';
import MarkdownComponent from '~/notebook/cells/markdown.vue';
+import Prompt from '~/notebook/cells/prompt.vue';
const Component = Vue.extend(MarkdownComponent);
window.katex = katex;
-function buildCellComponent(cell, relativePath = '') {
+function buildCellComponent(cell, relativePath = '', hidePrompt) {
return mount(Component, {
propsData: {
cell,
+ hidePrompt,
},
provide: {
relativeRawPath: relativePath,
},
- }).vm;
+ });
}
function buildMarkdownComponent(markdownContent, relativePath = '') {
@@ -33,7 +35,7 @@ function buildMarkdownComponent(markdownContent, relativePath = '') {
}
describe('Markdown component', () => {
- let vm;
+ let wrapper;
let cell;
let json;
@@ -43,21 +45,30 @@ describe('Markdown component', () => {
// eslint-disable-next-line prefer-destructuring
cell = json.cells[1];
- vm = buildCellComponent(cell);
+ wrapper = buildCellComponent(cell);
await nextTick();
});
- it('does not render prompt', () => {
- expect(vm.$el.querySelector('.prompt span')).toBeNull();
+ const findPrompt = () => wrapper.findComponent(Prompt);
+
+ it('renders a prompt by default', () => {
+ expect(findPrompt().exists()).toBe(true);
+ });
+
+ it('does not render a prompt if hidePrompt is true', () => {
+ wrapper = buildCellComponent(cell, '', true);
+ expect(findPrompt().exists()).toBe(false);
});
it('does not render the markdown text', () => {
- expect(vm.$el.querySelector('.markdown').innerHTML.trim()).not.toEqual(cell.source.join(''));
+ expect(wrapper.vm.$el.querySelector('.markdown').innerHTML.trim()).not.toEqual(
+ cell.source.join(''),
+ );
});
it('renders the markdown HTML', () => {
- expect(vm.$el.querySelector('.markdown h1')).not.toBeNull();
+ expect(wrapper.vm.$el.querySelector('.markdown h1')).not.toBeNull();
});
it('sanitizes Markdown output', async () => {
@@ -68,11 +79,11 @@ describe('Markdown component', () => {
});
await nextTick();
- expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull();
+ expect(wrapper.vm.$el.querySelector('a').getAttribute('href')).toBeNull();
});
it('sanitizes HTML', async () => {
- const findLink = () => vm.$el.querySelector('.xss-link');
+ const findLink = () => wrapper.vm.$el.querySelector('.xss-link');
Object.assign(cell, {
source: ['<a href="test.js" data-remote=true data-type="script" class="xss-link">XSS</a>\n'],
});
@@ -97,11 +108,11 @@ describe('Markdown component', () => {
["for embedded images, it doesn't", '![](data:image/jpeg;base64)\n', 'src="data:'],
["for images urls, it doesn't", '![](http://image.png)\n', 'src="http:'],
])('%s', async ([testMd, mustContain]) => {
- vm = buildMarkdownComponent([testMd], '/raw/');
+ wrapper = buildMarkdownComponent([testMd], '/raw/');
await nextTick();
- expect(vm.$el.innerHTML).toContain(mustContain);
+ expect(wrapper.vm.$el.innerHTML).toContain(mustContain);
});
});
@@ -111,13 +122,13 @@ describe('Markdown component', () => {
});
it('renders images and text', async () => {
- vm = buildCellComponent(json.cells[0]);
+ wrapper = buildCellComponent(json.cells[0]);
await nextTick();
- const images = vm.$el.querySelectorAll('img');
+ const images = wrapper.vm.$el.querySelectorAll('img');
expect(images.length).toBe(5);
- const columns = vm.$el.querySelectorAll('td');
+ const columns = wrapper.vm.$el.querySelectorAll('td');
expect(columns.length).toBe(6);
expect(columns[0].textContent).toEqual('Hello ');
@@ -141,81 +152,93 @@ describe('Markdown component', () => {
});
it('renders multi-line katex', async () => {
- vm = buildCellComponent(json.cells[0]);
+ wrapper = buildCellComponent(json.cells[0]);
await nextTick();
- expect(vm.$el.querySelector('.katex')).not.toBeNull();
+ expect(wrapper.vm.$el.querySelector('.katex')).not.toBeNull();
});
it('renders inline katex', async () => {
- vm = buildCellComponent(json.cells[1]);
+ wrapper = buildCellComponent(json.cells[1]);
await nextTick();
- expect(vm.$el.querySelector('p:first-child .katex')).not.toBeNull();
+ expect(wrapper.vm.$el.querySelector('p:first-child .katex')).not.toBeNull();
});
it('renders multiple inline katex', async () => {
- vm = buildCellComponent(json.cells[1]);
+ wrapper = buildCellComponent(json.cells[1]);
await nextTick();
- expect(vm.$el.querySelectorAll('p:nth-child(2) .katex')).toHaveLength(4);
+ expect(wrapper.vm.$el.querySelectorAll('p:nth-child(2) .katex')).toHaveLength(4);
});
it('output cell in case of katex error', async () => {
- vm = buildMarkdownComponent(['Some invalid $a & b$ inline formula $b & c$\n', '\n']);
+ wrapper = buildMarkdownComponent(['Some invalid $a & b$ inline formula $b & c$\n', '\n']);
await nextTick();
// expect one paragraph with no katex formula in it
- expect(vm.$el.querySelectorAll('p')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('p .katex')).toHaveLength(0);
+ expect(wrapper.vm.$el.querySelectorAll('p')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('p .katex')).toHaveLength(0);
});
it('output cell and render remaining formula in case of katex error', async () => {
- vm = buildMarkdownComponent([
+ wrapper = buildMarkdownComponent([
'An invalid $a & b$ inline formula and a vaild one $b = c$\n',
'\n',
]);
await nextTick();
// expect one paragraph with no katex formula in it
- expect(vm.$el.querySelectorAll('p')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('p .katex')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('p')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('p .katex')).toHaveLength(1);
});
it('renders math formula in list object', async () => {
- vm = buildMarkdownComponent(["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n']);
+ wrapper = buildMarkdownComponent([
+ "- list with inline $a=2$ inline formula $a' + b = c$\n",
+ '\n',
+ ]);
await nextTick();
// expect one list with a katex formula in it
- expect(vm.$el.querySelectorAll('li')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
+ expect(wrapper.vm.$el.querySelectorAll('li')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
});
it("renders math formula with tick ' in it", async () => {
- vm = buildMarkdownComponent(["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n']);
+ wrapper = buildMarkdownComponent([
+ "- list with inline $a=2$ inline formula $a' + b = c$\n",
+ '\n',
+ ]);
await nextTick();
// expect one list with a katex formula in it
- expect(vm.$el.querySelectorAll('li')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
+ expect(wrapper.vm.$el.querySelectorAll('li')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
});
it('renders math formula with less-than-operator < in it', async () => {
- vm = buildMarkdownComponent(['- list with inline $a=2$ inline formula $a + b < c$\n', '\n']);
+ wrapper = buildMarkdownComponent([
+ '- list with inline $a=2$ inline formula $a + b < c$\n',
+ '\n',
+ ]);
await nextTick();
// expect one list with a katex formula in it
- expect(vm.$el.querySelectorAll('li')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
+ expect(wrapper.vm.$el.querySelectorAll('li')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
});
it('renders math formula with greater-than-operator > in it', async () => {
- vm = buildMarkdownComponent(['- list with inline $a=2$ inline formula $a + b > c$\n', '\n']);
+ wrapper = buildMarkdownComponent([
+ '- list with inline $a=2$ inline formula $a + b > c$\n',
+ '\n',
+ ]);
await nextTick();
// expect one list with a katex formula in it
- expect(vm.$el.querySelectorAll('li')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
+ expect(wrapper.vm.$el.querySelectorAll('li')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
});
});
});
diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js
index 8bf049235a9..585cbb68eeb 100644
--- a/spec/frontend/notebook/cells/output/index_spec.js
+++ b/spec/frontend/notebook/cells/output/index_spec.js
@@ -1,12 +1,15 @@
import { mount } from '@vue/test-utils';
import json from 'test_fixtures/blob/notebook/basic.json';
import Output from '~/notebook/cells/output/index.vue';
+import MarkdownOutput from '~/notebook/cells/output/markdown.vue';
+import { relativeRawPath, markdownCellContent } from '../../mock_data';
describe('Output component', () => {
let wrapper;
const createComponent = (output) => {
wrapper = mount(Output, {
+ provide: { relativeRawPath },
propsData: {
outputs: [].concat(output),
count: 1,
@@ -95,6 +98,17 @@ describe('Output component', () => {
});
});
+ describe('Markdown output', () => {
+ beforeEach(() => {
+ const markdownType = { data: { 'text/markdown': markdownCellContent } };
+ createComponent(markdownType);
+ });
+
+ it('renders a markdown component', () => {
+ expect(wrapper.findComponent(MarkdownOutput).props('rawCode')).toBe(markdownCellContent);
+ });
+ });
+
describe('default to plain text', () => {
beforeEach(() => {
const unknownType = json.cells[6];
diff --git a/spec/frontend/notebook/cells/output/markdown_spec.js b/spec/frontend/notebook/cells/output/markdown_spec.js
new file mode 100644
index 00000000000..e3490ed3bea
--- /dev/null
+++ b/spec/frontend/notebook/cells/output/markdown_spec.js
@@ -0,0 +1,44 @@
+import { mount } from '@vue/test-utils';
+import MarkdownOutput from '~/notebook/cells/output/markdown.vue';
+import Prompt from '~/notebook/cells/prompt.vue';
+import Markdown from '~/notebook/cells/markdown.vue';
+import { relativeRawPath, markdownCellContent } from '../../mock_data';
+
+describe('markdown output cell', () => {
+ let wrapper;
+
+ const createComponent = ({ count = 0, index = 0 } = {}) => {
+ wrapper = mount(MarkdownOutput, {
+ provide: { relativeRawPath },
+ propsData: {
+ rawCode: markdownCellContent,
+ count,
+ index,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ const findPrompt = () => wrapper.findComponent(Prompt);
+ const findMarkdown = () => wrapper.findComponent(Markdown);
+
+ it.each`
+ index | count | showOutput
+ ${0} | ${1} | ${true}
+ ${1} | ${2} | ${false}
+ ${2} | ${3} | ${false}
+ `('renders a prompt', ({ index, count, showOutput }) => {
+ createComponent({ count, index });
+ expect(findPrompt().props()).toMatchObject({ count, showOutput, type: 'Out' });
+ });
+
+ it('renders a Markdown component', () => {
+ expect(findMarkdown().props()).toMatchObject({
+ cell: { source: markdownCellContent },
+ hidePrompt: true,
+ });
+ });
+});
diff --git a/spec/frontend/notebook/mock_data.js b/spec/frontend/notebook/mock_data.js
new file mode 100644
index 00000000000..b1419e1256f
--- /dev/null
+++ b/spec/frontend/notebook/mock_data.js
@@ -0,0 +1,2 @@
+export const relativeRawPath = '/test';
+export const markdownCellContent = ['# Test'];
diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js
index 1ff5766b074..b1e9d8d1256 100644
--- a/spec/frontend/releases/components/asset_links_form_spec.js
+++ b/spec/frontend/releases/components/asset_links_form_spec.js
@@ -292,6 +292,42 @@ describe('Release edit component', () => {
});
});
+ describe('remove button state', () => {
+ describe('when there is only one link', () => {
+ beforeEach(() => {
+ factory({
+ release: {
+ ...release,
+ assets: {
+ links: release.assets.links.slice(0, 1),
+ },
+ },
+ });
+ });
+
+ it('remove asset link button should not be present', () => {
+ expect(wrapper.find('.remove-button').exists()).toBe(false);
+ });
+ });
+
+ describe('when there are multiple links', () => {
+ beforeEach(() => {
+ factory({
+ release: {
+ ...release,
+ assets: {
+ links: release.assets.links.slice(0, 2),
+ },
+ },
+ });
+ });
+
+ it('remove asset link button should be visible', () => {
+ expect(wrapper.find('.remove-button').exists()).toBe(true);
+ });
+ });
+ });
+
describe('empty state', () => {
describe('when the release fetched from the API has no links', () => {
beforeEach(() => {
@@ -325,12 +361,6 @@ describe('Release edit component', () => {
it('does not call the addEmptyAssetLink store method when the component is created', () => {
expect(actions.addEmptyAssetLink).not.toHaveBeenCalled();
});
-
- it('calls addEmptyAssetLink when the final link is deleted by the user', () => {
- wrapper.find('.remove-button').vm.$emit('click');
-
- expect(actions.addEmptyAssetLink).toHaveBeenCalledTimes(1);
- });
});
});
});
diff --git a/spec/graphql/graphql_triggers_spec.rb b/spec/graphql/graphql_triggers_spec.rb
index a4a643582f5..a54cb8a7988 100644
--- a/spec/graphql/graphql_triggers_spec.rb
+++ b/spec/graphql/graphql_triggers_spec.rb
@@ -3,76 +3,89 @@
require 'spec_helper'
RSpec.describe GraphqlTriggers do
+ let_it_be(:issuable, refind: true) { create(:work_item) }
+
describe '.issuable_assignees_updated' do
- it 'triggers the issuableAssigneesUpdated subscription' do
- assignees = create_list(:user, 2)
- issue = create(:issue, assignees: assignees)
+ let(:assignees) { create_list(:user, 2) }
+ before do
+ issuable.update!(assignees: assignees)
+ end
+
+ it 'triggers the issuableAssigneesUpdated subscription' do
expect(GitlabSchema.subscriptions).to receive(:trigger).with(
'issuableAssigneesUpdated',
- { issuable_id: issue.to_gid },
- issue
+ { issuable_id: issuable.to_gid },
+ issuable
)
- GraphqlTriggers.issuable_assignees_updated(issue)
+ GraphqlTriggers.issuable_assignees_updated(issuable)
end
end
describe '.issuable_title_updated' do
it 'triggers the issuableTitleUpdated subscription' do
- work_item = create(:work_item)
-
expect(GitlabSchema.subscriptions).to receive(:trigger).with(
'issuableTitleUpdated',
- { issuable_id: work_item.to_gid },
- work_item
+ { issuable_id: issuable.to_gid },
+ issuable
).and_call_original
- GraphqlTriggers.issuable_title_updated(work_item)
+ GraphqlTriggers.issuable_title_updated(issuable)
end
end
describe '.issuable_description_updated' do
it 'triggers the issuableDescriptionUpdated subscription' do
- work_item = create(:work_item)
-
expect(GitlabSchema.subscriptions).to receive(:trigger).with(
'issuableDescriptionUpdated',
- { issuable_id: work_item.to_gid },
- work_item
+ { issuable_id: issuable.to_gid },
+ issuable
).and_call_original
- GraphqlTriggers.issuable_description_updated(work_item)
+ GraphqlTriggers.issuable_description_updated(issuable)
end
end
describe '.issuable_labels_updated' do
- it 'triggers the issuableLabelsUpdated subscription' do
- project = create(:project)
- labels = create_list(:label, 3, project: project)
- issue = create(:issue, labels: labels)
+ let(:labels) { create_list(:label, 3, project: create(:project)) }
+
+ before do
+ issuable.update!(labels: labels)
+ end
+ it 'triggers the issuableLabelsUpdated subscription' do
expect(GitlabSchema.subscriptions).to receive(:trigger).with(
'issuableLabelsUpdated',
- { issuable_id: issue.to_gid },
- issue
+ { issuable_id: issuable.to_gid },
+ issuable
)
- GraphqlTriggers.issuable_labels_updated(issue)
+ GraphqlTriggers.issuable_labels_updated(issuable)
end
end
describe '.issuable_dates_updated' do
it 'triggers the issuableDatesUpdated subscription' do
- work_item = create(:work_item)
-
expect(GitlabSchema.subscriptions).to receive(:trigger).with(
'issuableDatesUpdated',
- { issuable_id: work_item.to_gid },
- work_item
+ { issuable_id: issuable.to_gid },
+ issuable
+ ).and_call_original
+
+ GraphqlTriggers.issuable_dates_updated(issuable)
+ end
+ end
+
+ describe '.issuable_milestone_updated' do
+ it 'triggers the issuableMilestoneUpdated subscription' do
+ expect(GitlabSchema.subscriptions).to receive(:trigger).with(
+ 'issuableMilestoneUpdated',
+ { issuable_id: issuable.to_gid },
+ issuable
).and_call_original
- GraphqlTriggers.issuable_dates_updated(work_item)
+ GraphqlTriggers.issuable_milestone_updated(issuable)
end
end
diff --git a/spec/graphql/types/subscription_type_spec.rb b/spec/graphql/types/subscription_type_spec.rb
index c23a14deaf3..04f0c72b06f 100644
--- a/spec/graphql/types/subscription_type_spec.rb
+++ b/spec/graphql/types/subscription_type_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe GitlabSchema.types['Subscription'] do
issuable_description_updated
issuable_labels_updated
issuable_dates_updated
+ issuable_milestone_updated
merge_request_reviewers_updated
merge_request_merge_status_updated
]
diff --git a/spec/initializers/hashie_mash_permitted_patch_spec.rb b/spec/initializers/hashie_mash_permitted_patch_spec.rb
new file mode 100644
index 00000000000..0e9f8a485ff
--- /dev/null
+++ b/spec/initializers/hashie_mash_permitted_patch_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Hashie::Mash#permitted patch' do
+ let(:mash) { Hashie::Mash.new }
+
+ before do
+ load Rails.root.join('config/initializers/hashie_mash_permitted_patch.rb')
+ end
+
+ describe '#respond_to? with :permitted?' do
+ it 'returns false' do
+ expect(Gitlab::AppLogger).to receive(:info).with(
+ { message: 'Hashie::Mash#respond_to?(:permitted?)', caller: instance_of(Array) })
+
+ expect(mash.respond_to?(:permitted?)).to be false
+ end
+ end
+
+ describe '#permitted' do
+ it 'raises ArgumentError' do
+ expect(Gitlab::AppLogger).to receive(:info).with(
+ { message: 'Hashie::Mash#permitted?', caller: instance_of(Array) })
+
+ expect { mash.permitted? }.to raise_error(ArgumentError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/cookie_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/cookie_spec.rb
deleted file mode 100644
index 10e642b7713..00000000000
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/cookie_spec.rb
+++ /dev/null
@@ -1,130 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::Cookie, :clean_gitlab_redis_shared_state,
-:clean_gitlab_redis_queues do
- describe 'serialization' do
- it 'can round-trip a hash' do
- h = { 'hello' => 'world', 'foo' => 'bar' }
- expect(described_class.deserialize(described_class.serialize(h))).to eq(h)
- end
-
- it 'can merge by concatenating' do
- h1 = { 'foo' => 'bar', 'baz' => 'qux' }
- h2 = { 'foo' => 'other bar', 'hello' => 'world' }
- concatenated = described_class.serialize(h1) + described_class.serialize(h2)
- expect(described_class.deserialize(concatenated)).to eq(h1.merge(h2))
- end
- end
-
- shared_examples 'with Redis persistence' do
- let(:cookie) { described_class.new(key) }
- let(:key) { 'redis_key' }
- let(:hash) { { 'hello' => 'world' } }
-
- describe '.set' do
- subject { cookie.set(hash, expiry) }
-
- let(:expiry) { 10 }
-
- it 'stores the hash' do
- expect(subject).to be_truthy
- with_redis do |redis|
- expect(redis.get(key)).to eq("hello=world\n")
- expect(redis.ttl(key)).to be_within(1).of(expiry)
- end
- end
-
- context 'when the key is set' do
- before do
- with_redis { |r| r.set(key, 'foobar') }
- end
-
- it 'does not overwrite existing keys' do
- expect(subject).to be_falsey
- with_redis do |redis|
- expect(redis.get(key)).to eq('foobar')
- expect(redis.ttl(key)).to eq(-1)
- end
- end
- end
- end
-
- describe '.get' do
- subject { cookie.get }
-
- it { expect(subject).to eq({}) }
-
- context 'when the key is set' do
- before do
- with_redis { |r| r.set(key, "hello=world\n") }
- end
-
- it { expect(subject).to eq({ 'hello' => 'world' }) }
- end
- end
-
- describe '.append' do
- subject { cookie.append(hash) }
-
- it 'does not create the key' do
- subject
-
- with_redis do |redis|
- expect(redis.get(key)).to eq(nil)
- end
- end
-
- context 'when the key exists' do
- before do
- with_redis { |r| r.set(key, 'existing data', ex: 10) }
- end
-
- it 'appends without modifying ttl' do
- subject
-
- with_redis do |redis|
- expect(redis.get(key)).to eq("existing datahello=world\n")
- expect(redis.ttl(key)).to be_within(1).of(10)
- end
- end
- end
- end
- end
-
- context 'with multi-store feature flags turned on' do
- def with_redis(&block)
- Gitlab::Redis::DuplicateJobs.with(&block)
- end
-
- it 'use Gitlab::Redis::DuplicateJobs.with' do
- expect(Gitlab::Redis::DuplicateJobs).to receive(:with).and_call_original
- expect(Sidekiq).not_to receive(:redis)
-
- described_class.new('hello').get
- end
-
- it_behaves_like 'with Redis persistence'
- end
-
- context 'when both multi-store feature flags are off' do
- def with_redis(&block)
- Sidekiq.redis(&block)
- end
-
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_duplicate_jobs: false)
- stub_feature_flags(use_primary_store_as_default_for_duplicate_jobs: false)
- end
-
- it 'use Sidekiq.redis' do
- expect(Sidekiq).to receive(:redis).and_call_original
- expect(Gitlab::Redis::DuplicateJobs).not_to receive(:with)
-
- described_class.new('hello').get
- end
-
- it_behaves_like 'with Redis persistence'
- end
-end
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
index abe083f911b..1e8cb76905b 100644
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
@@ -527,9 +527,8 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
end
context 'with Redis cookies' do
- let(:cookie_key) do
- "#{idempotency_key}:cookie"
- end
+ let(:cookie_key) { "#{idempotency_key}:cookie" }
+ let(:cookie) { get_redis_msgpack(cookie_key) }
def with_redis(&block)
Gitlab::Redis::DuplicateJobs.with(&block)
@@ -541,15 +540,16 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
shared_examples 'sets Redis keys with correct TTL' do
it "adds an idempotency key with correct ttl" do
- expected_cookie = <<~COOKIE
- jid=123
- existing_wal_location:main=#{wal_locations['main']}
- existing_wal_location:ci=#{wal_locations['ci']}
- COOKIE
- expect { duplicate_job.check! }
- .to change { read_idempotency_key_with_ttl(cookie_key) }
- .from([nil, -2])
- .to([expected_cookie, be_within(1).of(expected_ttl)])
+ expected_cookie = {
+ 'jid' => '123',
+ 'offsets' => {},
+ 'wal_locations' => {},
+ 'existing_wal_locations' => wal_locations
+ }
+
+ duplicate_job.check!
+ expect(cookie).to eq(expected_cookie)
+ expect(redis_ttl(cookie_key)).to be_within(1).of(expected_ttl)
end
end
@@ -576,15 +576,17 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
context 'when there was already a job with same arguments in the same queue' do
before do
- set_idempotency_key(cookie_key, "jid=existing-jid\n")
+ set_idempotency_key(cookie_key, existing_cookie.to_msgpack)
end
+ let(:existing_cookie) { { 'jid' => 'existing-jid' } }
+
it { expect(duplicate_job.check!).to eq('existing-jid') }
it "does not change the existing key's TTL" do
expect { duplicate_job.check! }
- .not_to change { read_idempotency_key_with_ttl(cookie_key) }
- .from(["jid=existing-jid\n", -1])
+ .not_to change { redis_ttl(cookie_key) }
+ .from(-1)
end
it 'sets the existing jid' do
@@ -601,20 +603,22 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
{ main: ::ActiveRecord::Base,
ci: ::ActiveRecord::Base })
- set_idempotency_key(cookie_key, initial_cookie)
+ with_redis { |r| r.set(cookie_key, initial_cookie.to_msgpack, ex: expected_ttl) }
# read existing_wal_locations
duplicate_job.check!
end
let(:initial_cookie) do
- <<~COOKIE
- jid=foobar
- existing_wal_location:main=0/D525E3A0
- existing_wal_location:ci=AB/12340
- COOKIE
+ {
+ 'jid' => 'foobar',
+ 'existing_wal_locations' => { 'main' => '0/D525E3A0', 'ci' => 'AB/12340' },
+ 'offsets' => {},
+ 'wal_locations' => {}
+ }
end
+ let(:expected_ttl) { 123 }
let(:new_wal) do
{
# offset is relative to `existing_wal`
@@ -626,34 +630,76 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
let(:wal_locations) { new_wal.transform_values { |v| v[:location] } }
it 'stores a wal location to redis with an offset relative to existing wal location' do
- expected_cookie = initial_cookie + <<~COOKIE
- wal_location:main:#{new_wal['main'][:offset]}=#{new_wal['main'][:location]}
- wal_location:ci:#{new_wal['ci'][:offset]}=#{new_wal['ci'][:location]}
- COOKIE
+ duplicate_job.update_latest_wal_location!
+
+ expect(cookie['wal_locations']).to eq(wal_locations)
+ expect(cookie['offsets']).to eq(new_wal.transform_values { |v| v[:offset].to_i })
+ expect(redis_ttl(cookie_key)).to be_within(1).of(expected_ttl)
+ end
+ end
+
+ describe 'UPDATE_WAL_COOKIE_SCRIPT' do
+ subject do
+ with_redis do |redis|
+ redis.eval(described_class::UPDATE_WAL_COOKIE_SCRIPT, keys: [cookie_key], argv: argv)
+ end
+ end
+
+ let(:argv) { ['c1', 1, 'loc1', 'c2', 2, 'loc2', 'c3', 3, 'loc3'] }
+
+ it 'does not create the key' do
+ subject
+
+ expect(with_redis { |r| r.get(cookie_key) }).to eq(nil)
+ end
+
+ context 'when the key exists' do
+ let(:existing_cookie) { { 'offsets' => {}, 'wal_locations' => {} } }
+ let(:expected_ttl) { 123 }
+
+ before do
+ with_redis { |r| r.set(cookie_key, existing_cookie.to_msgpack, ex: expected_ttl) }
+ end
+
+ it 'updates all connections' do
+ subject
+
+ expect(cookie['wal_locations']).to eq({ 'c1' => 'loc1', 'c2' => 'loc2', 'c3' => 'loc3' })
+ expect(cookie['offsets']).to eq({ 'c1' => 1, 'c2' => 2, 'c3' => 3 })
+ end
+
+ it 'preserves the ttl' do
+ subject
+
+ expect(redis_ttl(cookie_key)).to be_within(1).of(expected_ttl)
+ end
- expect { duplicate_job.update_latest_wal_location! }
- .to change { read_idempotency_key_with_ttl(cookie_key) }
- .from([initial_cookie, -1])
- .to([expected_cookie, -1])
+ context 'and low offsets' do
+ let(:existing_cookie) do
+ {
+ 'offsets' => { 'c1' => 0, 'c2' => 2 },
+ 'wal_locations' => { 'c1' => 'loc1old', 'c2' => 'loc2old' }
+ }
+ end
+
+ it 'updates only some connections' do
+ subject
+
+ expect(cookie['wal_locations']).to eq({ 'c1' => 'loc1', 'c2' => 'loc2old', 'c3' => 'loc3' })
+ expect(cookie['offsets']).to eq({ 'c1' => 1, 'c2' => 2, 'c3' => 3 })
+ end
+ end
end
end
describe '#latest_wal_locations' do
context 'when job was deduplicated and wal locations were already persisted' do
before do
- cookie = <<~COOKIE
- jid=foobar
- wal_location:main:1=main1
- wal_location:ci:2:=ci2
- wal_location:main:5=main5
- wal_location:ci:6=ci6
- wal_location:main:3=main3
- wal_location:ci:4=ci4
- COOKIE
+ cookie = { 'wal_locations' => { 'main' => 'abc', 'ci' => 'def' } }.to_msgpack
set_idempotency_key(cookie_key, cookie)
end
- it { expect(duplicate_job.latest_wal_locations).to eq({ 'main' => 'main5', 'ci' => 'ci6' }) }
+ it { expect(duplicate_job.latest_wal_locations).to eq({ 'main' => 'abc', 'ci' => 'def' }) }
end
context 'when job is not deduplication and wal locations were not persisted' do
@@ -668,30 +714,22 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
context 'when the key exists in redis' do
before do
- set_idempotency_key(cookie_key, "jid=existing-jid\n")
+ set_idempotency_key(cookie_key, "garbage")
end
shared_examples 'deleting the duplicate job' do
shared_examples 'deleting keys from redis' do |key_name|
it "removes the #{key_name} from redis" do
expect { duplicate_job.delete! }
- .to change { read_idempotency_key_with_ttl(key) }
- .from([from_value, -1])
- .to([nil, -2])
- end
- end
-
- shared_examples 'does not delete key from redis' do |key_name|
- it "does not remove the #{key_name} from redis" do
- expect { duplicate_job.delete! }
- .to not_change { read_idempotency_key_with_ttl(key) }
- .from([from_value, -1])
+ .to change { with_redis { |r| r.get(key) } }
+ .from(from_value)
+ .to(nil)
end
end
it_behaves_like 'deleting keys from redis', 'cookie key' do
let(:key) { cookie_key }
- let(:from_value) { "jid=existing-jid\n" }
+ let(:from_value) { "garbage" }
end
end
@@ -730,8 +768,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
it 'sets the key in Redis' do
duplicate_job.set_deduplicated_flag!
- cookie = with_redis { |redis| redis.get(cookie_key) }
- expect(cookie).to include("\ndeduplicated=1\n")
+ expect(cookie['deduplicated']).to eq('1')
end
it 'sets, gets and cleans up the deduplicated flag' do
@@ -754,9 +791,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
duplicate_job.check!
duplicate_job.set_deduplicated_flag!
- cookie = with_redis { |redis| redis.get(cookie_key) }
-
- expect(cookie).not_to include('deduplicated=')
+ expect(cookie['deduplicated']).to eq(nil)
end
it 'does not set the deduplicated flag' do
@@ -783,7 +818,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
end
it 'returns true if the existing jid is different from the job jid' do
- set_idempotency_key(cookie_key, "jid=a different jid\n")
+ set_idempotency_key(cookie_key, { 'jid' => 'a different jid' }.to_msgpack)
duplicate_job.check!
expect(duplicate_job.duplicate?).to be(true)
@@ -794,13 +829,12 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
with_redis { |r| r.set(key, value) }
end
- def read_idempotency_key_with_ttl(key)
- with_redis do |redis|
- redis.pipelined do |p|
- p.get(key)
- p.ttl(key)
- end
- end
+ def get_redis_msgpack(key)
+ MessagePack.unpack(with_redis { |redis| redis.get(key) })
+ end
+
+ def redis_ttl(key)
+ with_redis { |redis| redis.ttl(key) }
end
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 20b1a1f58bb..ebc870406e8 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -104,10 +104,33 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(issue.issue_customer_relations_contacts.last.contact).to eq contact
end
- it 'updates issue milestone when passing `milestone` param' do
- update_issue(milestone: milestone)
+ context 'when updating milestone' do
+ before do
+ update_issue({ milestone: nil })
+ end
- expect(issue.milestone).to eq milestone
+ it 'updates issue milestone when passing `milestone` param' do
+ expect { update_issue({ milestone: milestone }) }
+ .to change(issue, :milestone).to(milestone).from(nil)
+ end
+
+ it "triggers 'issuableMilestoneUpdated'" do
+ expect(GraphqlTriggers).to receive(:issuable_milestone_updated).with(issue).and_call_original
+
+ update_issue({ milestone: milestone })
+ end
+
+ context 'when milestone remains unchanged' do
+ before do
+ update_issue({ title: 'abc', milestone: milestone })
+ end
+
+ it "does not trigger 'issuableMilestoneUpdated'" do
+ expect(GraphqlTriggers).not_to receive(:issuable_milestone_updated)
+
+ update_issue({ milestone: milestone })
+ end
+ end
end
context 'when sentry identifier is given' do
diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb
index 1761d1104dd..68efb4c220b 100644
--- a/spec/services/work_items/update_service_spec.rb
+++ b/spec/services/work_items/update_service_spec.rb
@@ -311,6 +311,34 @@ RSpec.describe WorkItems::UpdateService do
end
end
end
+
+ context 'for milestone widget' do
+ let_it_be(:milestone) { create(:milestone, project: project) }
+
+ let(:widget_params) { { milestone_widget: { milestone_id: milestone.id } } }
+
+ context 'when milestone is updated' do
+ it "triggers 'issuableMilestoneUpdated'" do
+ expect(work_item.milestone).to eq(nil)
+ expect(GraphqlTriggers).to receive(:issuable_milestone_updated).with(work_item).and_call_original
+
+ update_work_item
+ end
+ end
+
+ context 'when milestone remains unchanged' do
+ before do
+ update_work_item
+ end
+
+ it "does not trigger 'issuableMilestoneUpdated'" do
+ expect(work_item.milestone).to eq(milestone)
+ expect(GraphqlTriggers).not_to receive(:issuable_milestone_updated)
+
+ update_work_item
+ end
+ end
+ end
end
describe 'label updates' do