diff --git a/package.json b/package.json index 1d392818..94de8910 100644 --- a/package.json +++ b/package.json @@ -107,9 +107,8 @@ "setupTestFrameworkScriptFile": "/resources/assets/tests/setup.js", "coveragePathIgnorePatterns": [ "/node_modules/", - "setup", - "utils", - "assets/src/js" + "/resources/assets/tests/setup", + "/resources/assets/tests/utils" ], "testRegex": "resources/assets/tests/.*\\.(spec|test)\\.js" }, diff --git a/resources/assets/src/js/net.js b/resources/assets/src/js/net.js index 053b7266..3ef6c629 100644 --- a/resources/assets/src/js/net.js +++ b/resources/assets/src/js/net.js @@ -1,9 +1,8 @@ import Vue from 'vue'; +import { emit } from './event'; import { queryStringify } from './utils'; import { showAjaxError } from './notify'; -const csrfField = document.querySelector('meta[name="csrf-token"]'); - const empty = Object.create(null); /** @type Request */ export const init = { @@ -11,11 +10,19 @@ export const init = { headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', - 'X-CSRF-TOKEN': csrfField && csrfField.content } }; +function retrieveToken() { + const csrfField = document.querySelector('meta[name="csrf-token"]'); + return csrfField && csrfField.content; +} + export async function walkFetch(request) { + request.headers['X-CSRF-TOKEN'] = retrieveToken(); + + emit('beforeFetch', request); + try { const response = await fetch(request); if (response.ok) { diff --git a/resources/assets/src/js/notify.js b/resources/assets/src/js/notify.js index 900e8cd6..ccb7eda0 100644 --- a/resources/assets/src/js/notify.js +++ b/resources/assets/src/js/notify.js @@ -11,7 +11,7 @@ import { trans } from './i18n'; * @return {void} */ export function showMsg(msg, type = 'info') { - $('[id=msg]') + $('#msg') .removeClass() .addClass('callout') .addClass(`callout-${type}`) @@ -30,7 +30,7 @@ export function showAjaxError(error) { } const message = typeof error === 'string' ? error : error.message; - showModal(message.replace(/\n/g, '
'), trans('general.fatalError'), 'danger'); + showModal(message.replace(/\n/g, '
'), trans('general.fatalError'), 'danger'); } /** @@ -68,7 +68,7 @@ export function showModal(msg, title = 'Message', type = 'default', options = {} `; - $(dom).on('hidden.bs.modal', function () { + $(dom).on('hidden.bs.modal', /* istanbul ignore next */ function () { destroyOnClose && $(this).remove(); }).modal(options); } diff --git a/resources/assets/tests/js/net.test.js b/resources/assets/tests/js/net.test.js new file mode 100644 index 00000000..526377ac --- /dev/null +++ b/resources/assets/tests/js/net.test.js @@ -0,0 +1,78 @@ +import * as net from '@/js/net'; +import { on } from '@/js/event'; +import { showAjaxError } from '@/js/notify'; + +jest.mock('@/js/notify'); + +test('the GET method', async () => { + const json = jest.fn().mockResolvedValue({}); + window.fetch = jest.fn().mockResolvedValue({ + ok: true, + json + }); + window.Request = jest.fn(function (url, init) { + this.url = url; + Object.keys(init).forEach(key => this[key] = init[key]); + }); + + await net.get('/abc', { a: 'b' }); + expect(window.fetch.mock.calls[0][0].url).toBe('/abc?a=b'); + expect(json).toBeCalled(); + + await net.get('/abc'); + expect(window.fetch.mock.calls[1][0].url).toBe('/abc'); +}); + +test('the POST method', async () => { + window.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({}) + }); + window.Request = jest.fn(function (url, init) { + this.url = url; + Object.keys(init).forEach(key => this[key] = init[key]); + }); + + const meta = document.createElement('meta'); + meta.name = 'csrf-token'; + meta.content = 'token'; + document.head.appendChild(meta); + + await net.post('/abc', { a: 'b' }); + const request = window.fetch.mock.calls[0][0]; + expect(request.url).toBe('/abc'); + expect(request.method).toBe('POST'); + expect(request.body).toBe(JSON.stringify({ a: 'b' })); + expect(request.headers['X-CSRF-TOKEN']).toBe('token'); + + await net.post('/abc'); + expect(window.fetch.mock.calls[1][0].body).toBe('{}'); +}); + +test('low level fetch', async () => { + const json = jest.fn().mockResolvedValue({}); + window.fetch = jest.fn() + .mockRejectedValueOnce(new Error) + .mockResolvedValueOnce({ + ok: false, + text: () => Promise.resolve('404') + }) + .mockResolvedValueOnce({ + ok: true, + json + }); + + const stub = jest.fn(); + on('beforeFetch', stub); + const request = { headers: {} }; + + await net.walkFetch(request); + expect(showAjaxError.mock.calls[0][0]).toBeInstanceOf(Error); + expect(stub).toBeCalledWith(request); + + await net.walkFetch(request); + expect(showAjaxError).toBeCalledWith('404'); + + await net.walkFetch(request); + expect(json).toBeCalled(); +}); diff --git a/resources/assets/tests/js/notify.test.js b/resources/assets/tests/js/notify.test.js new file mode 100644 index 00000000..50d421d0 --- /dev/null +++ b/resources/assets/tests/js/notify.test.js @@ -0,0 +1,35 @@ +import $ from 'jquery'; +import * as notify from '@/js/notify'; + +test('show message', () => { + document.body.innerHTML = '
'; + notify.showMsg('hi'); + + const element = $('#msg'); + expect(element.hasClass('callout')).toBeTrue(); + expect(element.hasClass('callout-info')).toBeTrue(); + expect(element.html()).toBe('hi'); +}); + +test('show AJAX error', () => { + notify.showAjaxError(); // Can be no arguments + + $.fn.modal = function () { + document.body.innerHTML = this.html(); + }; + notify.showAjaxError('error\nerror'); + expect(document.body.innerHTML).toContain('error
error'); + + notify.showAjaxError(new Error('an-error')); + expect(document.body.innerHTML).toContain('an-error'); +}); + +test('show modal', () => { + notify.showModal('message'); + expect($('.modal-title').html()).toBe('Message'); + + notify.showModal('message', '', 'default', { + callback: () => undefined, + destroyOnClose: false + }); +}); diff --git a/resources/assets/tests/js/utils.test.js b/resources/assets/tests/js/utils.test.js new file mode 100644 index 00000000..d4984a0c --- /dev/null +++ b/resources/assets/tests/js/utils.test.js @@ -0,0 +1,23 @@ +import * as utils from '@/js/utils'; + +test('debounce', () => { + const stub = jest.fn(); + const debounced = utils.debounce(stub, 2000); + + debounced(); + debounced(); + expect(stub).not.toBeCalled(); + jest.runAllTimers(); + expect(stub).toBeCalledTimes(1); +}); + +test('queryString', () => { + history.pushState({}, 'page', 'about:blank?key=value'); + expect(utils.queryString('key')).toBe('value'); + expect(utils.queryString('a')).toBeUndefined(); + expect(utils.queryString('a', 'b')).toBe('b'); +}); + +test('queryStringify', () => { + expect(utils.queryStringify({ a: 'b', c: 'd' })).toBe('a=b&c=d'); +});