Commit 34c6c338 authored by Tomas Vik's avatar Tomas Vik Committed by Andrew Fontaine

Refactor IDE getRawFileData action

There wasn't any callback in the action and so we didn't have to
use new Promise((resolve,reject)=>{})
This will make it easier to mark toggle the file as being loaded
in the future
parent 5e781062
...@@ -185,7 +185,6 @@ export default { ...@@ -185,7 +185,6 @@ export default {
'setFileLanguage', 'setFileLanguage',
'setEditorPosition', 'setEditorPosition',
'setFileViewMode', 'setFileViewMode',
'updateViewer',
'removePendingTab', 'removePendingTab',
'triggerFilesChange', 'triggerFilesChange',
'addTempImage', 'addTempImage',
...@@ -241,7 +240,7 @@ export default { ...@@ -241,7 +240,7 @@ export default {
}); });
}, },
setupEditor() { setupEditor() {
if (!this.file || !this.editor.instance) return; if (!this.file || !this.editor.instance || this.file.loading) return;
const head = this.getStagedFile(this.file.path); const head = this.getStagedFile(this.file.path);
......
...@@ -65,7 +65,7 @@ export const getFileData = ( ...@@ -65,7 +65,7 @@ export const getFileData = (
if (file.raw || (file.tempFile && !file.prevPath && !fileDeletedAndReadded)) if (file.raw || (file.tempFile && !file.prevPath && !fileDeletedAndReadded))
return Promise.resolve(); return Promise.resolve();
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file, forceValue: true });
const url = joinPaths( const url = joinPaths(
gon.relative_url_root || '/', gon.relative_url_root || '/',
...@@ -84,10 +84,8 @@ export const getFileData = ( ...@@ -84,10 +84,8 @@ export const getFileData = (
if (data) commit(types.SET_FILE_DATA, { data, file }); if (data) commit(types.SET_FILE_DATA, { data, file });
if (openFile) commit(types.TOGGLE_FILE_OPEN, path); if (openFile) commit(types.TOGGLE_FILE_OPEN, path);
if (makeFileActive) dispatch('setFileActive', path); if (makeFileActive) dispatch('setFileActive', path);
commit(types.TOGGLE_LOADING, { entry: file });
}) })
.catch(() => { .catch(() => {
commit(types.TOGGLE_LOADING, { entry: file });
dispatch('setErrorMessage', { dispatch('setErrorMessage', {
text: __('An error occurred while loading the file.'), text: __('An error occurred while loading the file.'),
action: payload => action: payload =>
...@@ -95,6 +93,9 @@ export const getFileData = ( ...@@ -95,6 +93,9 @@ export const getFileData = (
actionText: __('Please try again'), actionText: __('Please try again'),
actionPayload: { path, makeFileActive }, actionPayload: { path, makeFileActive },
}); });
})
.finally(() => {
commit(types.TOGGLE_LOADING, { entry: file, forceValue: false });
}); });
}; };
...@@ -106,45 +107,41 @@ export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) = ...@@ -106,45 +107,41 @@ export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) =
const file = state.entries[path]; const file = state.entries[path];
const stagedFile = state.stagedFiles.find(f => f.path === path); const stagedFile = state.stagedFiles.find(f => f.path === path);
return new Promise((resolve, reject) => { const fileDeletedAndReadded = getters.isFileDeletedAndReadded(path);
const fileDeletedAndReadded = getters.isFileDeletedAndReadded(path); commit(types.TOGGLE_LOADING, { entry: file, forceValue: true });
service return service
.getRawFileData(fileDeletedAndReadded ? stagedFile : file) .getRawFileData(fileDeletedAndReadded ? stagedFile : file)
.then(raw => { .then(raw => {
if (!(file.tempFile && !file.prevPath && !fileDeletedAndReadded)) if (!(file.tempFile && !file.prevPath && !fileDeletedAndReadded))
commit(types.SET_FILE_RAW_DATA, { file, raw, fileDeletedAndReadded }); commit(types.SET_FILE_RAW_DATA, { file, raw, fileDeletedAndReadded });
if (file.mrChange && file.mrChange.new_file === false) { if (file.mrChange && file.mrChange.new_file === false) {
const baseSha = const baseSha =
(getters.currentMergeRequest && getters.currentMergeRequest.baseCommitSha) || ''; (getters.currentMergeRequest && getters.currentMergeRequest.baseCommitSha) || '';
service return service.getBaseRawFileData(file, baseSha).then(baseRaw => {
.getBaseRawFileData(file, baseSha) commit(types.SET_FILE_BASE_RAW_DATA, {
.then(baseRaw => { file,
commit(types.SET_FILE_BASE_RAW_DATA, { baseRaw,
file, });
baseRaw, return raw;
});
resolve(raw);
})
.catch(e => {
reject(e);
});
} else {
resolve(raw);
}
})
.catch(() => {
dispatch('setErrorMessage', {
text: __('An error occurred while loading the file content.'),
action: payload =>
dispatch('getRawFileData', payload).then(() => dispatch('setErrorMessage', null)),
actionText: __('Please try again'),
actionPayload: { path },
}); });
reject(); }
return raw;
})
.catch(e => {
dispatch('setErrorMessage', {
text: __('An error occurred while loading the file content.'),
action: payload =>
dispatch('getRawFileData', payload).then(() => dispatch('setErrorMessage', null)),
actionText: __('Please try again'),
actionPayload: { path },
}); });
}); throw e;
})
.finally(() => {
commit(types.TOGGLE_LOADING, { entry: file, forceValue: false });
});
}; };
export const changeFileContent = ({ commit, state, getters }, { path, content }) => { export const changeFileContent = ({ commit, state, getters }, { path, content }) => {
......
---
title: Resolve "WebIDE displays blank file incorrectly"
merge_request: 33391
type: fixed
/* useful for timing promises when jest fakeTimers are not reliable enough */
export default timeout =>
new Promise(resolve => {
jest.useRealTimers();
setTimeout(resolve, timeout);
jest.useFakeTimers();
});
...@@ -4,19 +4,25 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -4,19 +4,25 @@ import MockAdapter from 'axios-mock-adapter';
import '~/behaviors/markdown/render_gfm'; import '~/behaviors/markdown/render_gfm';
import { Range } from 'monaco-editor'; import { Range } from 'monaco-editor';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import service from '~/ide/services';
import { createStoreOptions } from '~/ide/stores'; import { createStoreOptions } from '~/ide/stores';
import RepoEditor from '~/ide/components/repo_editor.vue'; import RepoEditor from '~/ide/components/repo_editor.vue';
import Editor from '~/ide/lib/editor'; import Editor from '~/ide/lib/editor';
import { leftSidebarViews, FILE_VIEW_MODE_EDITOR, FILE_VIEW_MODE_PREVIEW } from '~/ide/constants'; import {
leftSidebarViews,
FILE_VIEW_MODE_EDITOR,
FILE_VIEW_MODE_PREVIEW,
viewerTypes,
} from '~/ide/constants';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { file } from '../helpers'; import { file } from '../helpers';
import { exampleConfigs, exampleFiles } from '../lib/editorconfig/mock_data'; import { exampleConfigs, exampleFiles } from '../lib/editorconfig/mock_data';
import waitUsingRealTimer from 'helpers/wait_using_real_timer';
describe('RepoEditor', () => { describe('RepoEditor', () => {
let vm; let vm;
let store; let store;
let mockActions;
const waitForEditorSetup = () => const waitForEditorSetup = () =>
new Promise(resolve => { new Promise(resolve => {
...@@ -30,6 +36,10 @@ describe('RepoEditor', () => { ...@@ -30,6 +36,10 @@ describe('RepoEditor', () => {
vm = createComponentWithStore(Vue.extend(RepoEditor), store, { vm = createComponentWithStore(Vue.extend(RepoEditor), store, {
file: store.state.openFiles[0], file: store.state.openFiles[0],
}); });
jest.spyOn(vm, 'getFileData').mockResolvedValue();
jest.spyOn(vm, 'getRawFileData').mockResolvedValue();
vm.$mount(); vm.$mount();
}; };
...@@ -43,21 +53,12 @@ describe('RepoEditor', () => { ...@@ -43,21 +53,12 @@ describe('RepoEditor', () => {
}; };
beforeEach(() => { beforeEach(() => {
mockActions = {
getFileData: jest.fn().mockResolvedValue(),
getRawFileData: jest.fn().mockResolvedValue(),
};
const f = { const f = {
...file(), ...file(),
viewMode: FILE_VIEW_MODE_EDITOR, viewMode: FILE_VIEW_MODE_EDITOR,
}; };
const storeOptions = createStoreOptions(); const storeOptions = createStoreOptions();
storeOptions.actions = {
...storeOptions.actions,
...mockActions,
};
store = new Vuex.Store(storeOptions); store = new Vuex.Store(storeOptions);
f.active = true; f.active = true;
...@@ -438,7 +439,7 @@ describe('RepoEditor', () => { ...@@ -438,7 +439,7 @@ describe('RepoEditor', () => {
vm.initEditor(); vm.initEditor();
vm.$nextTick() vm.$nextTick()
.then(() => { .then(() => {
expect(mockActions.getFileData).not.toHaveBeenCalled(); expect(vm.getFileData).not.toHaveBeenCalled();
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
...@@ -449,10 +450,11 @@ describe('RepoEditor', () => { ...@@ -449,10 +450,11 @@ describe('RepoEditor', () => {
vm.file.raw = ''; vm.file.raw = '';
vm.initEditor(); vm.initEditor();
vm.$nextTick() vm.$nextTick()
.then(() => { .then(() => {
expect(mockActions.getFileData).toHaveBeenCalled(); expect(vm.getFileData).toHaveBeenCalled();
expect(mockActions.getRawFileData).toHaveBeenCalled(); expect(vm.getRawFileData).toHaveBeenCalled();
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
...@@ -464,8 +466,8 @@ describe('RepoEditor', () => { ...@@ -464,8 +466,8 @@ describe('RepoEditor', () => {
vm.initEditor(); vm.initEditor();
vm.$nextTick() vm.$nextTick()
.then(() => { .then(() => {
expect(mockActions.getFileData).not.toHaveBeenCalled(); expect(vm.getFileData).not.toHaveBeenCalled();
expect(mockActions.getRawFileData).not.toHaveBeenCalled(); expect(vm.getRawFileData).not.toHaveBeenCalled();
expect(vm.editor.createInstance).not.toHaveBeenCalled(); expect(vm.editor.createInstance).not.toHaveBeenCalled();
}) })
.then(done) .then(done)
...@@ -526,6 +528,65 @@ describe('RepoEditor', () => { ...@@ -526,6 +528,65 @@ describe('RepoEditor', () => {
}); });
}); });
describe('populates editor with the fetched content', () => {
beforeEach(() => {
vm.getRawFileData.mockRestore();
});
const createRemoteFile = name => ({
...file(name),
tmpFile: false,
});
it('after switching viewer from edit to diff', async () => {
jest.spyOn(service, 'getRawFileData').mockImplementation(async () => {
expect(vm.file.loading).toBe(true);
// switching from edit to diff mode usually triggers editor initialization
store.state.viewer = viewerTypes.diff;
// we delay returning the file to make sure editor doesn't initialize before we fetch file content
await waitUsingRealTimer(10);
return 'rawFileData123\n';
});
const f = createRemoteFile('newFile');
Vue.set(store.state.entries, f.path, f);
vm.file = f;
// use the real timer to accurately simulate the race condition
await waitUsingRealTimer(20);
expect(vm.model.getModel().getValue()).toBe('rawFileData123\n');
});
it('after opening multiple files at the same time', async () => {
const fileA = createRemoteFile('fileA');
const fileB = createRemoteFile('fileB');
Vue.set(store.state.entries, fileA.path, fileA);
Vue.set(store.state.entries, fileB.path, fileB);
jest
.spyOn(service, 'getRawFileData')
.mockImplementationOnce(async () => {
// opening fileB while the content of fileA is still being fetched
vm.file = fileB;
return 'fileA-rawContent\n';
})
.mockImplementationOnce(async () => {
// we delay returning fileB content to make sure the editor doesn't initialize prematurely
await waitUsingRealTimer(10);
return 'fileB-rawContent\n';
});
vm.file = fileA;
// use the real timer to accurately simulate the race condition
await waitUsingRealTimer(20);
expect(vm.model.getModel().getValue()).toBe('fileB-rawContent\n');
});
});
describe('onPaste', () => { describe('onPaste', () => {
const setFileName = name => { const setFileName = name => {
Vue.set(vm, 'file', { Vue.set(vm, 'file', {
...@@ -632,8 +693,8 @@ describe('RepoEditor', () => { ...@@ -632,8 +693,8 @@ describe('RepoEditor', () => {
return waitForEditorSetup().then(() => { return waitForEditorSetup().then(() => {
expect(vm.rules).toEqual(monacoRules); expect(vm.rules).toEqual(monacoRules);
expect(vm.model.options).toMatchObject(monacoRules); expect(vm.model.options).toMatchObject(monacoRules);
expect(mockActions.getFileData).not.toHaveBeenCalled(); expect(vm.getFileData).not.toHaveBeenCalled();
expect(mockActions.getRawFileData).not.toHaveBeenCalled(); expect(vm.getRawFileData).not.toHaveBeenCalled();
}); });
}, },
); );
...@@ -649,13 +710,13 @@ describe('RepoEditor', () => { ...@@ -649,13 +710,13 @@ describe('RepoEditor', () => {
createComponent(); createComponent();
return waitForEditorSetup().then(() => { return waitForEditorSetup().then(() => {
expect(mockActions.getFileData.mock.calls.map(([, args]) => args)).toEqual([ expect(vm.getFileData.mock.calls.map(([args]) => args)).toEqual([
{ makeFileActive: false, path: 'foo/bar/baz/.editorconfig' }, { makeFileActive: false, path: 'foo/bar/baz/.editorconfig' },
{ makeFileActive: false, path: 'foo/bar/.editorconfig' }, { makeFileActive: false, path: 'foo/bar/.editorconfig' },
{ makeFileActive: false, path: 'foo/.editorconfig' }, { makeFileActive: false, path: 'foo/.editorconfig' },
{ makeFileActive: false, path: '.editorconfig' }, { makeFileActive: false, path: '.editorconfig' },
]); ]);
expect(mockActions.getRawFileData.mock.calls.map(([, args]) => args)).toEqual([ expect(vm.getRawFileData.mock.calls.map(([args]) => args)).toEqual([
{ path: 'foo/bar/baz/.editorconfig' }, { path: 'foo/bar/baz/.editorconfig' },
{ path: 'foo/bar/.editorconfig' }, { path: 'foo/bar/.editorconfig' },
{ path: 'foo/.editorconfig' }, { path: 'foo/.editorconfig' },
......
...@@ -446,6 +446,54 @@ describe('IDE store file actions', () => { ...@@ -446,6 +446,54 @@ describe('IDE store file actions', () => {
}) })
.catch(done.fail); .catch(done.fail);
}); });
describe('sets file loading to true', () => {
let loadingWhenGettingRawData;
let loadingWhenGettingBaseRawData;
beforeEach(() => {
loadingWhenGettingRawData = undefined;
loadingWhenGettingBaseRawData = undefined;
jest.spyOn(service, 'getRawFileData').mockImplementation(f => {
loadingWhenGettingRawData = f.loading;
return Promise.resolve('raw');
});
jest.spyOn(service, 'getBaseRawFileData').mockImplementation(f => {
loadingWhenGettingBaseRawData = f.loading;
return Promise.resolve('rawBase');
});
});
it('when getting raw file data', async () => {
expect(tmpFile.loading).toBe(false);
await store.dispatch('getRawFileData', { path: tmpFile.path });
expect(loadingWhenGettingRawData).toBe(true);
expect(tmpFile.loading).toBe(false);
});
it('when getting base raw file data', async () => {
tmpFile.mrChange = { new_file: false };
expect(tmpFile.loading).toBe(false);
await store.dispatch('getRawFileData', { path: tmpFile.path });
expect(loadingWhenGettingBaseRawData).toBe(true);
expect(tmpFile.loading).toBe(false);
});
it('when file was already loading', async () => {
tmpFile.loading = true;
await store.dispatch('getRawFileData', { path: tmpFile.path });
expect(loadingWhenGettingRawData).toBe(true);
expect(tmpFile.loading).toBe(false);
});
});
}); });
describe('return JSON', () => { describe('return JSON', () => {
...@@ -489,6 +537,12 @@ describe('IDE store file actions', () => { ...@@ -489,6 +537,12 @@ describe('IDE store file actions', () => {
}); });
}); });
}); });
it('toggles loading off after error', async () => {
await expect(store.dispatch('getRawFileData', { path: tmpFile.path })).rejects.toThrow();
expect(tmpFile.loading).toBe(false);
});
}); });
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment