app_spec.js 16.4 KB
Newer Older
1
/* eslint-disable no-unused-vars */
Regis Boudinot's avatar
Regis Boudinot committed
2
import Vue from 'vue';
Eric Eastwood's avatar
Eric Eastwood committed
3
import MockAdapter from 'axios-mock-adapter';
4 5
import setTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
import GLDropdown from '~/gl_dropdown';
Eric Eastwood's avatar
Eric Eastwood committed
6
import axios from '~/lib/utils/axios_utils';
7
import '~/behaviors/markdown/render_gfm';
8
import issuableApp from '~/issue_show/components/app.vue';
9
import eventHub from '~/issue_show/event_hub';
10
import { initialRequest, secondRequest } from '../mock_data';
Regis's avatar
Regis committed
11

12 13 14 15
function formatText(text) {
  return text.trim().replace(/\s\s+/g, ' ');
}

16
const REALTIME_REQUEST_STACK = [initialRequest, secondRequest];
Eric Eastwood's avatar
Eric Eastwood committed
17

18
describe('Issuable output', () => {
Eric Eastwood's avatar
Eric Eastwood committed
19 20 21
  let mock;
  let realtimeRequestCount = 0;
  let vm;
Filipa Lacerda's avatar
Filipa Lacerda committed
22

Mike Greiling's avatar
Mike Greiling committed
23
  beforeEach(done => {
24 25
    setFixtures(`
      <div>
26 27 28 29 30 31 32 33
        <div class="detail-page-description content-block">
        <details open>
          <summary>One</summary>
        </details>
        <details>
          <summary>Two</summary>
        </details>
      </div>
34 35 36 37
        <div class="flash-container"></div>
        <span id="task_status"></span>
      </div>
    `);
38
    spyOn(eventHub, '$emit');
39 40

    const IssuableDescriptionComponent = Vue.extend(issuableApp);
41

Eric Eastwood's avatar
Eric Eastwood committed
42
    mock = new MockAdapter(axios);
43 44 45 46 47 48 49
    mock
      .onGet('/gitlab-org/gitlab-shell/-/issues/9/realtime_changes/realtime_changes')
      .reply(() => {
        const res = Promise.resolve([200, REALTIME_REQUEST_STACK[realtimeRequestCount]]);
        realtimeRequestCount += 1;
        return res;
      });
Filipa Lacerda's avatar
Filipa Lacerda committed
50

51
    vm = new IssuableDescriptionComponent({
Regis Boudinot's avatar
Regis Boudinot committed
52
      propsData: {
53
        canUpdate: true,
54
        canDestroy: true,
55
        endpoint: '/gitlab-org/gitlab-shell/-/issues/9/realtime_changes',
Clement Ho's avatar
Clement Ho committed
56
        updateEndpoint: gl.TEST_HOST,
57
        issuableRef: '#1',
58 59
        initialTitleHtml: '',
        initialTitleText: '',
Clement Ho's avatar
Clement Ho committed
60 61
        initialDescriptionHtml: 'test',
        initialDescriptionText: 'test',
62
        lockVersion: 1,
63 64
        markdownPreviewPath: '/',
        markdownDocsPath: '/',
65 66
        projectNamespace: '/',
        projectPath: '/',
67
        issuableTemplateNamesPath: '/issuable-templates-path',
Regis Boudinot's avatar
Regis Boudinot committed
68 69
      },
    }).$mount();
Filipa Lacerda's avatar
Filipa Lacerda committed
70 71

    setTimeout(done);
72
  });
Regis Boudinot's avatar
Regis Boudinot committed
73

Filipa Lacerda's avatar
Filipa Lacerda committed
74
  afterEach(() => {
75
    mock.restore();
Eric Eastwood's avatar
Eric Eastwood committed
76
    realtimeRequestCount = 0;
77

Filipa Lacerda's avatar
Filipa Lacerda committed
78
    vm.poll.stop();
79
    vm.$destroy();
Filipa Lacerda's avatar
Filipa Lacerda committed
80 81
  });

Mike Greiling's avatar
Mike Greiling committed
82
  it('should render a title/description/edited and update title/description/edited on update', done => {
83 84
    let editedText;
    Vue.nextTick()
Mike Greiling's avatar
Mike Greiling committed
85 86 87 88 89 90
      .then(() => {
        editedText = vm.$el.querySelector('.edited-text');
      })
      .then(() => {
        expect(document.querySelector('title').innerText).toContain('this is a title (#1)');
        expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>this is a title</p>');
91
        expect(vm.$el.querySelector('.md').innerHTML).toContain('<p>this is a description!</p>');
Mike Greiling's avatar
Mike Greiling committed
92 93 94
        expect(vm.$el.querySelector('.js-task-list-field').value).toContain(
          'this is a description',
        );
Mike Greiling's avatar
Mike Greiling committed
95

Mike Greiling's avatar
Mike Greiling committed
96 97 98
        expect(formatText(editedText.innerText)).toMatch(/Edited[\s\S]+?by Some User/);
        expect(editedText.querySelector('.author-link').href).toMatch(/\/some_user$/);
        expect(editedText.querySelector('time')).toBeTruthy();
99
        expect(vm.state.lock_version).toEqual(1);
Mike Greiling's avatar
Mike Greiling committed
100 101 102 103 104 105 106 107
      })
      .then(() => {
        vm.poll.makeRequest();
      })
      .then(() => new Promise(resolve => setTimeout(resolve)))
      .then(() => {
        expect(document.querySelector('title').innerText).toContain('2 (#1)');
        expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>');
108
        expect(vm.$el.querySelector('.md').innerHTML).toContain('<p>42</p>');
Mike Greiling's avatar
Mike Greiling committed
109 110 111 112 113
        expect(vm.$el.querySelector('.js-task-list-field').value).toContain('42');
        expect(vm.$el.querySelector('.edited-text')).toBeTruthy();
        expect(formatText(vm.$el.querySelector('.edited-text').innerText)).toMatch(
          /Edited[\s\S]+?by Other User/,
        );
Mike Greiling's avatar
Mike Greiling committed
114

Mike Greiling's avatar
Mike Greiling committed
115 116
        expect(editedText.querySelector('.author-link').href).toMatch(/\/other_user$/);
        expect(editedText.querySelector('time')).toBeTruthy();
117
        expect(vm.state.lock_version).toEqual(2);
Mike Greiling's avatar
Mike Greiling committed
118 119 120
      })
      .then(done)
      .catch(done.fail);
Regis Boudinot's avatar
Regis Boudinot committed
121
  });
122

Mike Greiling's avatar
Mike Greiling committed
123
  it('shows actions if permissions are correct', done => {
124 125 126
    vm.showForm = true;

    Vue.nextTick(() => {
Mike Greiling's avatar
Mike Greiling committed
127
      expect(vm.$el.querySelector('.btn')).not.toBeNull();
128 129 130 131 132

      done();
    });
  });

Mike Greiling's avatar
Mike Greiling committed
133
  it('does not show actions if permissions are incorrect', done => {
134 135 136 137
    vm.showForm = true;
    vm.canUpdate = false;

    Vue.nextTick(() => {
Mike Greiling's avatar
Mike Greiling committed
138
      expect(vm.$el.querySelector('.btn')).toBeNull();
139 140 141 142 143

      done();
    });
  });

Mike Greiling's avatar
Mike Greiling committed
144
  it('does not update formState if form is already open', done => {
145
    vm.updateAndShowForm();
146 147 148

    vm.state.titleText = 'testing 123';

149
    vm.updateAndShowForm();
150 151

    Vue.nextTick(() => {
Mike Greiling's avatar
Mike Greiling committed
152
      expect(vm.store.formState.title).not.toBe('testing 123');
153 154 155 156 157

      done();
    });
  });

158
  describe('updateIssuable', () => {
Mike Greiling's avatar
Mike Greiling committed
159
    it('fetches new data after update', done => {
160
      spyOn(vm, 'updateStoreState').and.callThrough();
161
      spyOn(vm.service, 'getData').and.callThrough();
162 163 164 165
      spyOn(vm.service, 'updateIssuable').and.returnValue(
        Promise.resolve({
          data: { web_url: window.location.pathname },
        }),
Mike Greiling's avatar
Mike Greiling committed
166
      );
167

Eric Eastwood's avatar
Eric Eastwood committed
168 169
      vm.updateIssuable()
        .then(() => {
170
          expect(vm.updateStoreState).toHaveBeenCalled();
Eric Eastwood's avatar
Eric Eastwood committed
171 172 173 174
          expect(vm.service.getData).toHaveBeenCalled();
        })
        .then(done)
        .catch(done.fail);
175 176
    });

Mike Greiling's avatar
Mike Greiling committed
177
    it('correctly updates issuable data', done => {
178 179 180 181
      spyOn(vm.service, 'updateIssuable').and.returnValue(
        Promise.resolve({
          data: { web_url: window.location.pathname },
        }),
Mike Greiling's avatar
Mike Greiling committed
182
      );
183

Eric Eastwood's avatar
Eric Eastwood committed
184 185 186 187 188 189 190
      vm.updateIssuable()
        .then(() => {
          expect(vm.service.updateIssuable).toHaveBeenCalledWith(vm.formState);
          expect(eventHub.$emit).toHaveBeenCalledWith('close.form');
        })
        .then(done)
        .catch(done.fail);
191 192
    });

Mike Greiling's avatar
Mike Greiling committed
193
    it('does not redirect if issue has not moved', done => {
194
      const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
195 196 197 198 199 200 201
      spyOn(vm.service, 'updateIssuable').and.returnValue(
        Promise.resolve({
          data: {
            web_url: window.location.pathname,
            confidential: vm.isConfidential,
          },
        }),
Mike Greiling's avatar
Mike Greiling committed
202
      );
203 204 205 206

      vm.updateIssuable();

      setTimeout(() => {
207
        expect(visitUrl).not.toHaveBeenCalled();
208 209 210 211
        done();
      });
    });

Mike Greiling's avatar
Mike Greiling committed
212
    it('redirects if returned web_url has changed', done => {
213
      const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
214 215 216 217 218 219 220
      spyOn(vm.service, 'updateIssuable').and.returnValue(
        Promise.resolve({
          data: {
            web_url: '/testing-issue-move',
            confidential: vm.isConfidential,
          },
        }),
Mike Greiling's avatar
Mike Greiling committed
221
      );
Phil Hughes's avatar
Phil Hughes committed
222 223 224 225

      vm.updateIssuable();

      setTimeout(() => {
226
        expect(visitUrl).toHaveBeenCalledWith('/testing-issue-move');
Phil Hughes's avatar
Phil Hughes committed
227 228 229 230
        done();
      });
    });

231
    describe('shows dialog when issue has unsaved changed', () => {
Mike Greiling's avatar
Mike Greiling committed
232
      it('confirms on title change', done => {
233 234 235 236 237 238
        vm.showForm = true;
        vm.state.titleText = 'title has changed';
        const e = { returnValue: null };
        vm.handleBeforeUnloadEvent(e);
        Vue.nextTick(() => {
          expect(e.returnValue).not.toBeNull();
239

240 241 242 243
          done();
        });
      });

Mike Greiling's avatar
Mike Greiling committed
244
      it('confirms on description change', done => {
245 246 247 248 249 250
        vm.showForm = true;
        vm.state.descriptionText = 'description has changed';
        const e = { returnValue: null };
        vm.handleBeforeUnloadEvent(e);
        Vue.nextTick(() => {
          expect(e.returnValue).not.toBeNull();
251

252 253 254 255
          done();
        });
      });

Mike Greiling's avatar
Mike Greiling committed
256
      it('does nothing when nothing has changed', done => {
257 258 259 260
        const e = { returnValue: null };
        vm.handleBeforeUnloadEvent(e);
        Vue.nextTick(() => {
          expect(e.returnValue).toBeNull();
261

262 263 264 265 266
          done();
        });
      });
    });

Clement Ho's avatar
Clement Ho committed
267
    describe('error when updating', () => {
Mike Greiling's avatar
Mike Greiling committed
268
      it('closes form on error', done => {
269
        spyOn(vm.service, 'updateIssuable').and.callFake(() => Promise.reject());
Clement Ho's avatar
Clement Ho committed
270
        vm.updateIssuable();
271

Clement Ho's avatar
Clement Ho committed
272
        setTimeout(() => {
273
          expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
274 275 276
          expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
            `Error updating issue`,
          );
277

Clement Ho's avatar
Clement Ho committed
278 279 280 281
          done();
        });
      });

Mike Greiling's avatar
Mike Greiling committed
282
      it('returns the correct error message for issuableType', done => {
283
        spyOn(vm.service, 'updateIssuable').and.callFake(() => Promise.reject());
Clement Ho's avatar
Clement Ho committed
284 285 286 287 288 289
        vm.issuableType = 'merge request';

        Vue.nextTick(() => {
          vm.updateIssuable();

          setTimeout(() => {
290
            expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
291 292 293
            expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
              `Error updating merge request`,
            );
Clement Ho's avatar
Clement Ho committed
294 295 296 297

            done();
          });
        });
298
      });
299

300
      it('shows error message from backend if exists', done => {
301
        const msg = 'Custom error message from backend';
302
        spyOn(vm.service, 'updateIssuable').and.callFake(
303 304
          // eslint-disable-next-line prefer-promise-reject-errors
          () => Promise.reject({ response: { data: { errors: [msg] } } }),
305 306 307 308
        );

        vm.updateIssuable();
        setTimeout(() => {
309 310 311
          expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
            `${vm.defaultErrorMessage}. ${msg}`,
          );
312 313 314 315

          done();
        });
      });
316 317 318
    });
  });

Mike Greiling's avatar
Mike Greiling committed
319
  it('opens recaptcha modal if update rejected as spam', done => {
320
    function mockScriptSrc() {
Mike Greiling's avatar
Mike Greiling committed
321
      const recaptchaChild = vm.$children.find(
Mike Greiling's avatar
Mike Greiling committed
322
        // eslint-disable-next-line no-underscore-dangle
Mike Greiling's avatar
Mike Greiling committed
323
        child => child.$options._componentTag === 'recaptcha-modal',
Mike Greiling's avatar
Mike Greiling committed
324
      );
325 326 327 328 329

      recaptchaChild.scriptSrc = '//scriptsrc';
    }

    let modal;
Mike Greiling's avatar
Mike Greiling committed
330
    const promise = new Promise(resolve => {
331
      resolve({
Eric Eastwood's avatar
Eric Eastwood committed
332 333
        data: {
          recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>',
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
        },
      });
    });

    spyOn(vm.service, 'updateIssuable').and.returnValue(promise);

    vm.canUpdate = true;
    vm.showForm = true;

    vm.$nextTick()
      .then(() => mockScriptSrc())
      .then(() => vm.updateIssuable())
      .then(promise)
      .then(() => setTimeoutPromise())
      .then(() => {
349
        modal = vm.$el.querySelector('.js-recaptcha-modal');
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364

        expect(modal.style.display).not.toEqual('none');
        expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html');
        expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc');
      })
      .then(() => modal.querySelector('.close').click())
      .then(() => vm.$nextTick())
      .then(() => {
        expect(modal.style.display).toEqual('none');
        expect(document.body.querySelector('.js-recaptcha-script')).toBeNull();
      })
      .then(done)
      .catch(done.fail);
  });

365
  describe('deleteIssuable', () => {
Mike Greiling's avatar
Mike Greiling committed
366
    it('changes URL when deleted', done => {
367
      const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
368 369 370 371 372 373
      spyOn(vm.service, 'deleteIssuable').and.returnValue(
        Promise.resolve({
          data: {
            web_url: '/test',
          },
        }),
Mike Greiling's avatar
Mike Greiling committed
374
      );
375 376 377 378

      vm.deleteIssuable();

      setTimeout(() => {
379
        expect(visitUrl).toHaveBeenCalledWith('/test');
380

381 382 383 384
        done();
      });
    });

Mike Greiling's avatar
Mike Greiling committed
385
    it('stops polling when deleting', done => {
386
      spyOnDependency(issuableApp, 'visitUrl');
Filipa Lacerda's avatar
Filipa Lacerda committed
387
      spyOn(vm.poll, 'stop').and.callThrough();
388 389 390 391 392 393
      spyOn(vm.service, 'deleteIssuable').and.returnValue(
        Promise.resolve({
          data: {
            web_url: '/test',
          },
        }),
Mike Greiling's avatar
Mike Greiling committed
394
      );
395 396 397 398

      vm.deleteIssuable();

      setTimeout(() => {
Mike Greiling's avatar
Mike Greiling committed
399
        expect(vm.poll.stop).toHaveBeenCalledWith();
400

401 402 403 404
        done();
      });
    });

Mike Greiling's avatar
Mike Greiling committed
405
    it('closes form on error', done => {
406
      spyOn(vm.service, 'deleteIssuable').and.returnValue(Promise.reject());
407 408 409 410

      vm.deleteIssuable();

      setTimeout(() => {
411
        expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
412 413 414
        expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
          'Error deleting issue',
        );
415 416 417 418 419

        done();
      });
    });
  });
420

421
  describe('updateAndShowForm', () => {
Mike Greiling's avatar
Mike Greiling committed
422
    it('shows locked warning if form is open & data is different', done => {
Eric Eastwood's avatar
Eric Eastwood committed
423
      vm.$nextTick()
424
        .then(() => {
425
          vm.updateAndShowForm();
426

Filipa Lacerda's avatar
Filipa Lacerda committed
427
          vm.poll.makeRequest();
428 429 430 431 432 433

          return new Promise(resolve => {
            vm.$watch('formState.lockedWarningVisible', value => {
              if (value) resolve();
            });
          });
434 435
        })
        .then(() => {
Eric Eastwood's avatar
Eric Eastwood committed
436
          expect(vm.formState.lockedWarningVisible).toEqual(true);
437
          expect(vm.formState.lock_version).toEqual(1);
Eric Eastwood's avatar
Eric Eastwood committed
438
          expect(vm.$el.querySelector('.alert')).not.toBeNull();
439
        })
440
        .then(done)
441 442 443
        .catch(done.fail);
    });
  });
444

445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
  describe('requestTemplatesAndShowForm', () => {
    beforeEach(() => {
      spyOn(vm, 'updateAndShowForm');
    });

    it('shows the form if template names request is successful', done => {
      const mockData = [{ name: 'Bug' }];
      mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));

      vm.requestTemplatesAndShowForm()
        .then(() => {
          expect(vm.updateAndShowForm).toHaveBeenCalledWith(mockData);
        })
        .then(done)
        .catch(done.fail);
    });

    it('shows the form if template names request failed', done => {
      mock
        .onGet('/issuable-templates-path')
        .reply(() => Promise.reject(new Error('something went wrong')));

      vm.requestTemplatesAndShowForm()
        .then(() => {
          expect(document.querySelector('.flash-container .flash-text').textContent).toContain(
            'Error updating issue',
          );

          expect(vm.updateAndShowForm).toHaveBeenCalledWith();
        })
        .then(done)
        .catch(done.fail);
    });
  });

480 481 482 483 484 485 486
  describe('show inline edit button', () => {
    it('should not render by default', () => {
      expect(vm.$el.querySelector('.title-container .note-action-button')).toBeDefined();
    });

    it('should render if showInlineEditButton', () => {
      vm.showInlineEditButton = true;
487

488 489 490
      expect(vm.$el.querySelector('.title-container .note-action-button')).toBeDefined();
    });
  });
491 492 493 494 495 496 497 498 499 500 501 502 503 504 505

  describe('updateStoreState', () => {
    it('should make a request and update the state of the store', done => {
      const data = { foo: 1 };
      spyOn(vm.store, 'updateState');
      spyOn(vm.service, 'getData').and.returnValue(Promise.resolve({ data }));

      vm.updateStoreState()
        .then(() => {
          expect(vm.service.getData).toHaveBeenCalled();
          expect(vm.store.updateState).toHaveBeenCalledWith(data);
        })
        .then(done)
        .catch(done.fail);
    });
Fatih Acet's avatar
Fatih Acet committed
506 507 508 509 510 511 512

    it('should show error message if store update fails', done => {
      spyOn(vm.service, 'getData').and.returnValue(Promise.reject());
      vm.issuableType = 'merge request';

      vm.updateStoreState()
        .then(() => {
513 514 515
          expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
            `Error updating ${vm.issuableType}`,
          );
Fatih Acet's avatar
Fatih Acet committed
516 517 518 519
        })
        .then(done)
        .catch(done.fail);
    });
520
  });
Rajat Jain's avatar
Rajat Jain committed
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567

  describe('issueChanged', () => {
    beforeEach(() => {
      vm.store.formState.title = '';
      vm.store.formState.description = '';
      vm.initialDescriptionText = '';
      vm.initialTitleText = '';
    });

    it('returns true when title is changed', () => {
      vm.store.formState.title = 'RandomText';

      expect(vm.issueChanged).toBe(true);
    });

    it('returns false when title is empty null', () => {
      vm.store.formState.title = null;

      expect(vm.issueChanged).toBe(false);
    });

    it('returns false when `initialTitleText` is null and `formState.title` is empty string', () => {
      vm.store.formState.title = '';
      vm.initialTitleText = null;

      expect(vm.issueChanged).toBe(false);
    });

    it('returns true when description is changed', () => {
      vm.store.formState.description = 'RandomText';

      expect(vm.issueChanged).toBe(true);
    });

    it('returns false when description is empty null', () => {
      vm.store.formState.title = null;

      expect(vm.issueChanged).toBe(false);
    });

    it('returns false when `initialDescriptionText` is null and `formState.description` is empty string', () => {
      vm.store.formState.description = '';
      vm.initialDescriptionText = null;

      expect(vm.issueChanged).toBe(false);
    });
  });
Regis Boudinot's avatar
Regis Boudinot committed
568
});