Commit 1cb050a9 authored by Frédéric Caplette's avatar Frédéric Caplette

Merge branch '281686-use-proj-data' into 'master'

Make use of the project data served by Rails

See merge request gitlab-org/gitlab!72218
parents 0ecf4a5c 4d1eb551
...@@ -29,14 +29,20 @@ export default { ...@@ -29,14 +29,20 @@ export default {
}, },
}, },
watch: { watch: {
showLoading(newVal) { showLoading() {
if (!newVal) { this.notifyTreeReady();
this.$emit('tree-ready');
}
}, },
}, },
mounted() {
this.notifyTreeReady();
},
methods: { methods: {
...mapActions(['toggleTreeOpen']), ...mapActions(['toggleTreeOpen']),
notifyTreeReady() {
if (!this.showLoading) {
this.$emit('tree-ready');
}
},
clickedFile() { clickedFile() {
performanceMarkAndMeasure({ mark: WEBIDE_MARK_FILE_CLICKED }); performanceMarkAndMeasure({ mark: WEBIDE_MARK_FILE_CLICKED });
}, },
......
import Vue from 'vue'; import Vue from 'vue';
import createFlash from '~/flash';
import IdeRouter from '~/ide/ide_router_extension'; import IdeRouter from '~/ide/ide_router_extension';
import { joinPaths } from '~/lib/utils/url_utility'; import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { import {
WEBIDE_MARK_FETCH_PROJECT_DATA_START, WEBIDE_MARK_FETCH_PROJECT_DATA_START,
WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH, WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH,
...@@ -75,49 +73,34 @@ export const createRouter = (store, defaultBranch) => { ...@@ -75,49 +73,34 @@ export const createRouter = (store, defaultBranch) => {
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
if (to.params.namespace && to.params.project) { if (to.params.namespace && to.params.project) {
performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_PROJECT_DATA_START }); const basePath = to.params.pathMatch || '';
store const projectId = `${to.params.namespace}/${to.params.project}`;
.dispatch('getProjectData', { const branchId = to.params.branchid;
namespace: to.params.namespace, const mergeRequestId = to.params.mrid;
projectId: to.params.project,
})
.then(() => {
const basePath = to.params.pathMatch || '';
const projectId = `${to.params.namespace}/${to.params.project}`;
const branchId = to.params.branchid;
const mergeRequestId = to.params.mrid;
if (branchId) { performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_PROJECT_DATA_START });
performanceMarkAndMeasure({ if (branchId) {
mark: WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH, performanceMarkAndMeasure({
measures: [ mark: WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH,
{ measures: [
name: WEBIDE_MEASURE_FETCH_PROJECT_DATA, {
start: WEBIDE_MARK_FETCH_PROJECT_DATA_START, name: WEBIDE_MEASURE_FETCH_PROJECT_DATA,
}, start: WEBIDE_MARK_FETCH_PROJECT_DATA_START,
], },
}); ],
store.dispatch('openBranch', { });
projectId, store.dispatch('openBranch', {
branchId, projectId,
basePath, branchId,
}); basePath,
} else if (mergeRequestId) { });
store.dispatch('openMergeRequest', { } else if (mergeRequestId) {
projectId, store.dispatch('openMergeRequest', {
mergeRequestId, projectId,
targetProjectId: to.query.target_project, mergeRequestId,
}); targetProjectId: to.query.target_project,
}
})
.catch((e) => {
createFlash({
message: __('Error while loading the project data. Please try again.'),
fadeTransition: false,
addBodyClass: true,
});
throw e;
}); });
}
} }
next(); next();
......
...@@ -34,11 +34,18 @@ Vue.use(PerformancePlugin, { ...@@ -34,11 +34,18 @@ Vue.use(PerformancePlugin, {
* @param {extendStoreCallback} options.extendStore - * @param {extendStoreCallback} options.extendStore -
* Function that receives the default store and returns an extended one. * Function that receives the default store and returns an extended one.
*/ */
export function initIde(el, options = {}) { export const initIde = (el, options = {}) => {
if (!el) return null; if (!el) return null;
const { rootComponent = ide, extendStore = identity } = options; const { rootComponent = ide, extendStore = identity } = options;
const store = createStore(); const store = createStore();
const project = JSON.parse(el.dataset.project);
store.dispatch('setProject', { project });
// fire and forget fetching non-critical project info
store.dispatch('fetchProjectPermissions');
const router = createRouter(store, el.dataset.defaultBranch || DEFAULT_BRANCH); const router = createRouter(store, el.dataset.defaultBranch || DEFAULT_BRANCH);
return new Vue({ return new Vue({
...@@ -77,7 +84,7 @@ export function initIde(el, options = {}) { ...@@ -77,7 +84,7 @@ export function initIde(el, options = {}) {
return createElement(rootComponent); return createElement(rootComponent);
}, },
}); });
} };
/** /**
* Start the IDE. * Start the IDE.
......
import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql';
import Api from '~/api'; import Api from '~/api';
import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql';
import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql'; import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.query.graphql'; import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.query.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { query, mutate } from './gql'; import { query, mutate } from './gql';
const fetchApiProjectData = (projectPath) => Api.project(projectPath).then(({ data }) => data);
const fetchGqlProjectData = (projectPath) =>
query({
query: getIdeProject,
variables: { projectPath },
}).then(({ data }) => ({
...data.project,
id: getIdFromGraphQLId(data.project.id),
}));
export default { export default {
getFileData(endpoint) { getFileData(endpoint) {
return axios.get(endpoint, { return axios.get(endpoint, {
...@@ -65,18 +54,6 @@ export default { ...@@ -65,18 +54,6 @@ export default {
) )
.then(({ data }) => data); .then(({ data }) => data);
}, },
getProjectData(namespace, project) {
const projectPath = `${namespace}/${project}`;
return Promise.all([fetchApiProjectData(projectPath), fetchGqlProjectData(projectPath)]).then(
([apiProjectData, gqlProjectData]) => ({
data: {
...apiProjectData,
...gqlProjectData,
},
}),
);
},
getProjectMergeRequests(projectId, params = {}) { getProjectMergeRequests(projectId, params = {}) {
return Api.projectMergeRequests(projectId, params); return Api.projectMergeRequests(projectId, params);
}, },
...@@ -119,4 +96,13 @@ export default { ...@@ -119,4 +96,13 @@ export default {
variables: { input: { featureName: name } }, variables: { input: { featureName: name } },
}).then(({ data }) => data); }).then(({ data }) => data);
}, },
getProjectPermissionsData(projectPath) {
return query({
query: getIdeProject,
variables: { projectPath },
}).then(({ data }) => ({
...data.project,
id: getIdFromGraphQLId(data.project.id),
}));
},
}; };
import { escape } from 'lodash'; import { escape } from 'lodash';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import { logError } from '~/lib/logger';
import api from '../../../api'; import api from '../../../api';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
export const getProjectData = ({ commit, state }, { namespace, projectId, force = false } = {}) => const ERROR_LOADING_PROJECT = __('Error loading project data. Please try again.');
new Promise((resolve, reject) => {
if (!state.projects[`${namespace}/${projectId}`] || force) { const errorFetchingData = (e) => {
commit(types.TOGGLE_LOADING, { entry: state }); logError(ERROR_LOADING_PROJECT, e);
service
.getProjectData(namespace, projectId) createFlash({
.then((res) => res.data) message: ERROR_LOADING_PROJECT,
.then((data) => { fadeTransition: false,
commit(types.TOGGLE_LOADING, { entry: state }); addBodyClass: true,
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
resolve(data);
})
.catch(() => {
createFlash({
message: __('Error loading project data. Please try again.'),
fadeTransition: false,
addBodyClass: true,
});
reject(new Error(`Project not loaded ${namespace}/${projectId}`));
});
} else {
resolve(state.projects[`${namespace}/${projectId}`]);
}
}); });
};
export const setProject = ({ commit }, { project } = {}) => {
if (!project) {
return;
}
const projectPath = project.path_with_namespace;
commit(types.SET_PROJECT, { projectPath, project });
commit(types.SET_CURRENT_PROJECT, projectPath);
};
export const fetchProjectPermissions = ({ commit, state }) => {
const projectPath = state.currentProjectId;
if (!projectPath) {
return undefined;
}
return service
.getProjectPermissionsData(projectPath)
.then((permissions) => {
commit(types.UPDATE_PROJECT, { projectPath, props: permissions });
})
.catch(errorFetchingData);
};
export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) => export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) =>
service service
......
...@@ -8,6 +8,7 @@ export const SET_LINKS = 'SET_LINKS'; ...@@ -8,6 +8,7 @@ export const SET_LINKS = 'SET_LINKS';
// Project Mutation Types // Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT'; export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT'; export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const UPDATE_PROJECT = 'UPDATE_PROJECT';
export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE'; export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
// Merge request mutation types // Merge request mutation types
......
import Vue from 'vue';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
export default { export default {
...@@ -24,4 +25,15 @@ export default { ...@@ -24,4 +25,15 @@ export default {
empty_repo: value, empty_repo: value,
}); });
}, },
[types.UPDATE_PROJECT](state, { projectPath, props }) {
const project = state.projects[projectPath];
if (!project || !props) {
return;
}
Object.keys(props).forEach((key) => {
Vue.set(project, key, props[key]);
});
},
}; };
...@@ -29,7 +29,7 @@ module IdeHelper ...@@ -29,7 +29,7 @@ module IdeHelper
def convert_to_project_entity_json(project) def convert_to_project_entity_json(project)
return unless project return unless project
API::Entities::Project.represent(project).to_json API::Entities::Project.represent(project, current_user: current_user).to_json
end end
def enable_environments_guidance? def enable_environments_guidance?
......
...@@ -13896,9 +13896,6 @@ msgstr "" ...@@ -13896,9 +13896,6 @@ msgstr ""
msgid "Error while loading the merge request. Please try again." msgid "Error while loading the merge request. Please try again."
msgstr "" msgstr ""
msgid "Error while loading the project data. Please try again."
msgstr ""
msgid "Error while migrating %{upload_id}: %{error_message}" msgid "Error while migrating %{upload_id}: %{error_message}"
msgstr "" msgstr ""
......
...@@ -38,9 +38,16 @@ describe('IDE tree list', () => { ...@@ -38,9 +38,16 @@ describe('IDE tree list', () => {
beforeEach(() => { beforeEach(() => {
bootstrapWithTree(); bootstrapWithTree();
jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.$mount(); vm.$mount();
}); });
it('emits tree-ready event', () => {
expect(vm.$emit).toHaveBeenCalledTimes(1);
expect(vm.$emit).toHaveBeenCalledWith('tree-ready');
});
it('renders loading indicator', (done) => { it('renders loading indicator', (done) => {
store.state.trees['abcproject/main'].loading = true; store.state.trees['abcproject/main'].loading = true;
...@@ -61,9 +68,15 @@ describe('IDE tree list', () => { ...@@ -61,9 +68,15 @@ describe('IDE tree list', () => {
beforeEach(() => { beforeEach(() => {
bootstrapWithTree(emptyBranchTree); bootstrapWithTree(emptyBranchTree);
jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.$mount(); vm.$mount();
}); });
it('still emits tree-ready event', () => {
expect(vm.$emit).toHaveBeenCalledWith('tree-ready');
});
it('does not load files if the branch is empty', () => { it('does not load files if the branch is empty', () => {
expect(vm.$el.textContent).not.toContain('fileName'); expect(vm.$el.textContent).not.toContain('fileName');
expect(vm.$el.textContent).toContain('No files'); expect(vm.$el.textContent).toContain('No files');
......
...@@ -6,6 +6,7 @@ describe('IDE router', () => { ...@@ -6,6 +6,7 @@ describe('IDE router', () => {
const PROJECT_NAMESPACE = 'my-group/sub-group'; const PROJECT_NAMESPACE = 'my-group/sub-group';
const PROJECT_NAME = 'my-project'; const PROJECT_NAME = 'my-project';
const TEST_PATH = `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/merge_requests/2`; const TEST_PATH = `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/merge_requests/2`;
const DEFAULT_BRANCH = 'default-main';
let store; let store;
let router; let router;
...@@ -13,34 +14,46 @@ describe('IDE router', () => { ...@@ -13,34 +14,46 @@ describe('IDE router', () => {
beforeEach(() => { beforeEach(() => {
window.history.replaceState({}, '', '/'); window.history.replaceState({}, '', '/');
store = createStore(); store = createStore();
router = createRouter(store); router = createRouter(store, DEFAULT_BRANCH);
jest.spyOn(store, 'dispatch').mockReturnValue(new Promise(() => {})); jest.spyOn(store, 'dispatch').mockReturnValue(new Promise(() => {}));
}); });
[ it.each`
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/blob/`, route | expectedBranchId | expectedBasePath
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/blob`, ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/blob/`} | ${'main'} | ${'src/blob/'}
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/blob/-/src/blob`, ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/blob`} | ${'main'} | ${'src/blob'}
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/tree/`, ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/blob/-/src/blob`} | ${'blob'} | ${'src/blob'}
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/weird:branch/name-123/-/src/tree/`, ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/tree/`} | ${'main'} | ${'src/tree/'}
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/blob`, ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/weird:branch/name-123/-/src/tree/`} | ${'weird:branch/name-123'} | ${'src/tree/'}
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/edit`, ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/blob`} | ${'main'} | ${'src/blob'}
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/merge_requests/2`, ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/edit`} | ${'main'} | ${'src/edit'}
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/blob/-/src/blob`, ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/merge_requests/2`} | ${'main'} | ${'src/merge_requests/2'}
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/edit/blob/-/src/blob`, ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/blob/-/src/blob`} | ${'blob'} | ${'src/blob'}
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/merge_requests/2`, ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/edit/blob/-/src/blob`} | ${'blob'} | ${'src/blob'}
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/blob`, ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/blob`} | ${'blob'} | ${''}
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/edit`, ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/edit`} | ${DEFAULT_BRANCH} | ${''}
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}`, ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}`} | ${DEFAULT_BRANCH} | ${''}
].forEach((route) => { `('correctly opens Web IDE for $route', ({ route, expectedBranchId, expectedBasePath } = {}) => {
it(`finds project path when route is "${route}"`, () => { router.push(route);
router.push(route);
expect(store.dispatch).toHaveBeenCalledWith('openBranch', {
expect(store.dispatch).toHaveBeenCalledWith('getProjectData', { projectId: `${PROJECT_NAMESPACE}/${PROJECT_NAME}`,
namespace: PROJECT_NAMESPACE, branchId: expectedBranchId,
projectId: PROJECT_NAME, basePath: expectedBasePath,
}); });
});
it('correctly opens an MR', () => {
const expectedId = '2';
router.push(`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/merge_requests/${expectedId}`);
expect(store.dispatch).toHaveBeenCalledWith('openMergeRequest', {
projectId: `${PROJECT_NAMESPACE}/${PROJECT_NAME}`,
mergeRequestId: expectedId,
targetProjectId: undefined,
}); });
expect(store.dispatch).not.toHaveBeenCalledWith('openBranch');
}); });
it('keeps router in sync when store changes', async () => { it('keeps router in sync when store changes', async () => {
......
...@@ -216,35 +216,6 @@ describe('IDE services', () => { ...@@ -216,35 +216,6 @@ describe('IDE services', () => {
); );
}); });
describe('getProjectData', () => {
it('combines gql and API requests', () => {
const gqlProjectData = {
id: 'gid://gitlab/Project/1',
userPermissions: {
bogus: true,
},
};
const expectedResponse = {
...projectData,
...gqlProjectData,
id: 1,
};
Api.project.mockReturnValue(Promise.resolve({ data: { ...projectData } }));
query.mockReturnValue(Promise.resolve({ data: { project: gqlProjectData } }));
return services.getProjectData(TEST_NAMESPACE, TEST_PROJECT).then((response) => {
expect(response).toEqual({ data: expectedResponse });
expect(Api.project).toHaveBeenCalledWith(TEST_PROJECT_ID);
expect(query).toHaveBeenCalledWith({
query: getIdeProject,
variables: {
projectPath: TEST_PROJECT_ID,
},
});
});
});
});
describe('getFiles', () => { describe('getFiles', () => {
let mock; let mock;
let relativeUrlRoot; let relativeUrlRoot;
...@@ -336,4 +307,38 @@ describe('IDE services', () => { ...@@ -336,4 +307,38 @@ describe('IDE services', () => {
}); });
}); });
}); });
describe('getProjectPermissionsData', () => {
const TEST_PROJECT_PATH = 'foo/bar';
it('queries for the project permissions', () => {
const result = { data: { project: projectData } };
query.mockResolvedValue(result);
return services.getProjectPermissionsData(TEST_PROJECT_PATH).then((data) => {
expect(data).toEqual(result.data.project);
expect(query).toHaveBeenCalledWith(
expect.objectContaining({
query: getIdeProject,
variables: { projectPath: TEST_PROJECT_PATH },
}),
);
});
});
it('converts the returned GraphQL id to the regular ID number', () => {
const projectId = 2;
const gqlProjectData = {
id: `gid://gitlab/Project/${projectId}`,
userPermissions: {
bogus: true,
},
};
query.mockResolvedValue({ data: { project: gqlProjectData } });
return services.getProjectPermissionsData(TEST_PROJECT_PATH).then((data) => {
expect(data.id).toBe(projectId);
});
});
});
}); });
...@@ -2,9 +2,12 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -2,9 +2,12 @@ import MockAdapter from 'axios-mock-adapter';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import api from '~/api'; import api from '~/api';
import createFlash from '~/flash';
import service from '~/ide/services'; import service from '~/ide/services';
import { createStore } from '~/ide/stores'; import { createStore } from '~/ide/stores';
import { import {
setProject,
fetchProjectPermissions,
refreshLastCommitData, refreshLastCommitData,
showBranchNotFoundError, showBranchNotFoundError,
createNewBranchFromDefault, createNewBranchFromDefault,
...@@ -13,8 +16,12 @@ import { ...@@ -13,8 +16,12 @@ import {
loadFile, loadFile,
loadBranch, loadBranch,
} from '~/ide/stores/actions'; } from '~/ide/stores/actions';
import { logError } from '~/lib/logger';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
jest.mock('~/flash');
jest.mock('~/lib/logger');
const TEST_PROJECT_ID = 'abc/def'; const TEST_PROJECT_ID = 'abc/def';
describe('IDE store project actions', () => { describe('IDE store project actions', () => {
...@@ -34,6 +41,92 @@ describe('IDE store project actions', () => { ...@@ -34,6 +41,92 @@ describe('IDE store project actions', () => {
mock.restore(); mock.restore();
}); });
describe('setProject', () => {
const project = { id: 'foo', path_with_namespace: TEST_PROJECT_ID };
const baseMutations = [
{
type: 'SET_PROJECT',
payload: {
projectPath: TEST_PROJECT_ID,
project,
},
},
{
type: 'SET_CURRENT_PROJECT',
payload: TEST_PROJECT_ID,
},
];
it.each`
desc | payload | expectedMutations
${'does not commit any action if project is not passed'} | ${undefined} | ${[]}
${'commits correct actions in the correct order by default'} | ${{ project }} | ${[...baseMutations]}
`('$desc', async ({ payload, expectedMutations } = {}) => {
await testAction({
action: setProject,
payload,
state: store.state,
expectedMutations,
expectedActions: [],
});
});
});
describe('fetchProjectPermissions', () => {
const permissionsData = {
userPermissions: {
bogus: true,
},
};
const permissionsMutations = [
{
type: 'UPDATE_PROJECT',
payload: {
projectPath: TEST_PROJECT_ID,
props: {
...permissionsData,
},
},
},
];
let spy;
beforeEach(() => {
spy = jest.spyOn(service, 'getProjectPermissionsData');
});
afterEach(() => {
createFlash.mockRestore();
});
it.each`
desc | projectPath | responseSuccess | expectedMutations
${'does not fetch permissions if project does not exist'} | ${undefined} | ${true} | ${[]}
${'fetches permission when project is specified'} | ${TEST_PROJECT_ID} | ${true} | ${[...permissionsMutations]}
${'flashes an error if the request fails'} | ${TEST_PROJECT_ID} | ${false} | ${[]}
`('$desc', async ({ projectPath, expectedMutations, responseSuccess } = {}) => {
store.state.currentProjectId = projectPath;
if (responseSuccess) {
spy.mockResolvedValue(permissionsData);
} else {
spy.mockRejectedValue();
}
await testAction({
action: fetchProjectPermissions,
state: store.state,
expectedMutations,
expectedActions: [],
});
if (!responseSuccess) {
expect(logError).toHaveBeenCalled();
expect(createFlash).toHaveBeenCalled();
}
});
});
describe('refreshLastCommitData', () => { describe('refreshLastCommitData', () => {
beforeEach(() => { beforeEach(() => {
store.state.currentProjectId = 'abc/def'; store.state.currentProjectId = 'abc/def';
......
...@@ -3,21 +3,48 @@ import state from '~/ide/stores/state'; ...@@ -3,21 +3,48 @@ import state from '~/ide/stores/state';
describe('Multi-file store branch mutations', () => { describe('Multi-file store branch mutations', () => {
let localState; let localState;
const nonExistentProj = 'nonexistent';
const existingProj = 'abcproject';
beforeEach(() => { beforeEach(() => {
localState = state(); localState = state();
localState.projects = { abcproject: { empty_repo: true } }; localState.projects = { [existingProj]: { empty_repo: true } };
}); });
describe('TOGGLE_EMPTY_STATE', () => { describe('TOGGLE_EMPTY_STATE', () => {
it('sets empty_repo for project to passed value', () => { it('sets empty_repo for project to passed value', () => {
mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: 'abcproject', value: false }); mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: existingProj, value: false });
expect(localState.projects.abcproject.empty_repo).toBe(false); expect(localState.projects[existingProj].empty_repo).toBe(false);
mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: 'abcproject', value: true }); mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: existingProj, value: true });
expect(localState.projects.abcproject.empty_repo).toBe(true); expect(localState.projects[existingProj].empty_repo).toBe(true);
});
});
describe('UPDATE_PROJECT', () => {
it.each`
desc | projectPath | props | expectedProps
${'extends existing project with the passed props'} | ${existingProj} | ${{ foo1: 'bar' }} | ${{ foo1: 'bar' }}
${'overrides existing props on the exsiting project'} | ${existingProj} | ${{ empty_repo: false }} | ${{ empty_repo: false }}
${'does nothing if the project does not exist'} | ${nonExistentProj} | ${{ foo2: 'bar' }} | ${undefined}
${'does nothing if project is not passed'} | ${undefined} | ${{ foo3: 'bar' }} | ${undefined}
${'does nothing if the props are not passed'} | ${existingProj} | ${undefined} | ${{}}
${'does nothing if the props are empty'} | ${existingProj} | ${{}} | ${{}}
`('$desc', ({ projectPath, props, expectedProps } = {}) => {
const origProject = localState.projects[projectPath];
mutations.UPDATE_PROJECT(localState, { projectPath, props });
if (!expectedProps) {
expect(localState.projects[projectPath]).toBeUndefined();
} else {
expect(localState.projects[projectPath]).toEqual({
...origProject,
...expectedProps,
});
}
}); });
}); });
}); });
...@@ -192,6 +192,13 @@ export const commit = async ({ newBranch = false, newMR = false, newBranchName = ...@@ -192,6 +192,13 @@ export const commit = async ({ newBranch = false, newMR = false, newBranchName =
switchLeftSidebarTab('Commit'); switchLeftSidebarTab('Commit');
screen.getByTestId('begin-commit-button').click(); screen.getByTestId('begin-commit-button').click();
await waitForMonacoEditor();
const mrCheck = await screen.findByLabelText('Start a new merge request');
if (Boolean(mrCheck.checked) !== newMR) {
mrCheck.click();
}
if (!newBranch) { if (!newBranch) {
const option = await screen.findByLabelText(/Commit to .+ branch/); const option = await screen.findByLabelText(/Commit to .+ branch/);
option.click(); option.click();
...@@ -201,12 +208,9 @@ export const commit = async ({ newBranch = false, newMR = false, newBranchName = ...@@ -201,12 +208,9 @@ export const commit = async ({ newBranch = false, newMR = false, newBranchName =
const branchNameInput = await screen.findByTestId('ide-new-branch-name'); const branchNameInput = await screen.findByTestId('ide-new-branch-name');
fireEvent.input(branchNameInput, { target: { value: newBranchName } }); fireEvent.input(branchNameInput, { target: { value: newBranchName } });
const mrCheck = await screen.findByLabelText('Start a new merge request');
if (Boolean(mrCheck.checked) !== newMR) {
mrCheck.click();
}
} }
screen.getByText('Commit').click(); screen.getByText('Commit').click();
await waitForMonacoEditor();
}; };
...@@ -4,16 +4,18 @@ import setWindowLocation from 'helpers/set_window_location_helper'; ...@@ -4,16 +4,18 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { initIde } from '~/ide'; import { initIde } from '~/ide';
import extendStore from '~/ide/stores/extend'; import extendStore from '~/ide/stores/extend';
import { getProject, getEmptyProject } from 'jest/../frontend_integration/test_helpers/fixtures';
import { IDE_DATASET } from './mock_data'; import { IDE_DATASET } from './mock_data';
export default (container, { isRepoEmpty = false, path = '', mrId = '' } = {}) => { export default (container, { isRepoEmpty = false, path = '', mrId = '' } = {}) => {
const projectName = isRepoEmpty ? 'lorem-ipsum-empty' : 'lorem-ipsum'; const projectName = isRepoEmpty ? 'lorem-ipsum-empty' : 'lorem-ipsum';
const pathSuffix = mrId ? `merge_requests/${mrId}` : `tree/master/-/${path}`; const pathSuffix = mrId ? `merge_requests/${mrId}` : `tree/master/-/${path}`;
const project = isRepoEmpty ? getEmptyProject() : getProject();
setWindowLocation(`${TEST_HOST}/-/ide/project/gitlab-test/${projectName}/${pathSuffix}`); setWindowLocation(`${TEST_HOST}/-/ide/project/gitlab-test/${projectName}/${pathSuffix}`);
const el = document.createElement('div'); const el = document.createElement('div');
Object.assign(el.dataset, IDE_DATASET); Object.assign(el.dataset, IDE_DATASET, { project: JSON.stringify(project) });
container.appendChild(el); container.appendChild(el);
const vm = initIde(el, { extendStore }); const vm = initIde(el, { extendStore });
......
...@@ -34,10 +34,10 @@ describe('IDE: User opens IDE', () => { ...@@ -34,10 +34,10 @@ describe('IDE: User opens IDE', () => {
expect(await screen.findByText('No files')).toBeDefined(); expect(await screen.findByText('No files')).toBeDefined();
}); });
it('shows a "New file" button', async () => { it('shows a "New file" button', () => {
const button = await screen.findByTitle('New file'); const buttons = screen.queryAllByTitle('New file');
expect(button.tagName).toEqual('BUTTON'); expect(buttons.map((x) => x.tagName)).toContain('BUTTON');
}); });
}); });
......
...@@ -34,7 +34,7 @@ RSpec.describe IdeHelper do ...@@ -34,7 +34,7 @@ RSpec.describe IdeHelper do
self.instance_variable_set(:@fork_info, fork_info) self.instance_variable_set(:@fork_info, fork_info)
self.instance_variable_set(:@project, project) self.instance_variable_set(:@project, project)
serialized_project = API::Entities::Project.represent(project).to_json serialized_project = API::Entities::Project.represent(project, current_user: project.creator).to_json
expect(helper.ide_data) expect(helper.ide_data)
.to include( .to include(
......
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