diff options
Diffstat (limited to 'spec/frontend_integration')
24 files changed, 430 insertions, 63 deletions
diff --git a/spec/frontend_integration/ide/ide_integration_spec.js b/spec/frontend_integration/ide/ide_integration_spec.js index 7e8fb3a32ee..91d89c26ec1 100644 --- a/spec/frontend_integration/ide/ide_integration_spec.js +++ b/spec/frontend_integration/ide/ide_integration_spec.js @@ -8,93 +8,55 @@ * * See https://gitlab.com/gitlab-org/gitlab/-/issues/208800 for more information. */ -import MockAdapter from 'axios-mock-adapter'; -import axios from '~/lib/utils/axios_utils'; +import { TEST_HOST } from 'helpers/test_constants'; +import { useOverclockTimers } from 'test_helpers/utils/overclock_timers'; import { initIde } from '~/ide'; - -jest.mock('~/api', () => { - return { - project: jest.fn().mockImplementation(() => new Promise(() => {})), - }; -}); - -jest.mock('~/ide/services/gql', () => { - return { - query: jest.fn().mockImplementation(() => new Promise(() => {})), - }; -}); +import extendStore from '~/ide/stores/extend'; + +const TEST_DATASET = { + emptyStateSvgPath: '/test/empty_state.svg', + noChangesStateSvgPath: '/test/no_changes_state.svg', + committedStateSvgPath: '/test/committed_state.svg', + pipelinesEmptyStateSvgPath: '/test/pipelines_empty_state.svg', + promotionSvgPath: '/test/promotion.svg', + ciHelpPagePath: '/test/ci_help_page', + webIDEHelpPagePath: '/test/web_ide_help_page', + clientsidePreviewEnabled: 'true', + renderWhitespaceInCode: 'false', + codesandboxBundlerUrl: 'test/codesandbox_bundler', +}; describe('WebIDE', () => { + useOverclockTimers(); + let vm; let root; - let mock; - let initData; - let location; beforeEach(() => { root = document.createElement('div'); - initData = { - emptyStateSvgPath: '/test/empty_state.svg', - noChangesStateSvgPath: '/test/no_changes_state.svg', - committedStateSvgPath: '/test/committed_state.svg', - pipelinesEmptyStateSvgPath: '/test/pipelines_empty_state.svg', - promotionSvgPath: '/test/promotion.svg', - ciHelpPagePath: '/test/ci_help_page', - webIDEHelpPagePath: '/test/web_ide_help_page', - clientsidePreviewEnabled: 'true', - renderWhitespaceInCode: 'false', - codesandboxBundlerUrl: 'test/codesandbox_bundler', - }; + document.body.appendChild(root); - mock = new MockAdapter(axios); - mock.onAny('*').reply(() => new Promise(() => {})); - - location = { pathname: '/-/ide/project/gitlab-test/test', search: '', hash: '' }; - Object.defineProperty(window, 'location', { - get() { - return location; - }, + global.jsdom.reconfigure({ + url: `${TEST_HOST}/-/ide/project/gitlab-test/lorem-ipsum`, }); }); afterEach(() => { vm.$destroy(); vm = null; - - mock.restore(); + root.remove(); }); const createComponent = () => { const el = document.createElement('div'); - Object.assign(el.dataset, initData); + Object.assign(el.dataset, TEST_DATASET); root.appendChild(el); - vm = initIde(el); + vm = initIde(el, { extendStore }); }; - expect.addSnapshotSerializer({ - test(value) { - return value instanceof HTMLElement && !value.$_hit; - }, - print(element, serialize) { - element.$_hit = true; - element.querySelectorAll('[style]').forEach(el => { - el.$_hit = true; - if (el.style.display === 'none') { - el.textContent = '(jest: contents hidden)'; - } - }); - - return serialize(element) - .replace(/^\s*<!---->$/gm, '') - .replace(/\n\s*\n/gm, '\n'); - }, - }); - it('runs', () => { createComponent(); - return vm.$nextTick().then(() => { - expect(root).toMatchSnapshot(); - }); + expect(root).toMatchSnapshot(); }); }); diff --git a/spec/frontend_integration/test_helpers/factories/commit.js b/spec/frontend_integration/test_helpers/factories/commit.js new file mode 100644 index 00000000000..1ee82e74ffe --- /dev/null +++ b/spec/frontend_integration/test_helpers/factories/commit.js @@ -0,0 +1,15 @@ +import { withValues } from '../utils/obj'; +import { getCommit } from '../fixtures'; +import { createCommitId } from './commit_id'; + +// eslint-disable-next-line import/prefer-default-export +export const createNewCommit = ({ id = createCommitId(), message }, orig = getCommit()) => { + return withValues(orig, { + id, + short_id: id.substr(0, 8), + message, + title: message, + web_url: orig.web_url.replace(orig.id, id), + parent_ids: [orig.id], + }); +}; diff --git a/spec/frontend_integration/test_helpers/factories/commit_id.js b/spec/frontend_integration/test_helpers/factories/commit_id.js new file mode 100644 index 00000000000..9fa278c9dde --- /dev/null +++ b/spec/frontend_integration/test_helpers/factories/commit_id.js @@ -0,0 +1,21 @@ +const COMMIT_ID_LENGTH = 40; +const DEFAULT_COMMIT_ID = Array(COMMIT_ID_LENGTH) + .fill('0') + .join(''); + +export const createCommitId = (index = 0) => + `${index}${DEFAULT_COMMIT_ID}`.substr(0, COMMIT_ID_LENGTH); + +export const createCommitIdGenerator = () => { + let prevCommitId = 0; + + const next = () => { + prevCommitId += 1; + + return createCommitId(prevCommitId); + }; + + return { + next, + }; +}; diff --git a/spec/frontend_integration/test_helpers/factories/index.js b/spec/frontend_integration/test_helpers/factories/index.js new file mode 100644 index 00000000000..0f28830b236 --- /dev/null +++ b/spec/frontend_integration/test_helpers/factories/index.js @@ -0,0 +1,2 @@ +export * from './commit'; +export * from './commit_id'; diff --git a/spec/frontend_integration/test_helpers/fixtures.js b/spec/frontend_integration/test_helpers/fixtures.js new file mode 100644 index 00000000000..5f9c0e8dcba --- /dev/null +++ b/spec/frontend_integration/test_helpers/fixtures.js @@ -0,0 +1,10 @@ +/* eslint-disable global-require */ +import { memoize } from 'lodash'; + +export const getProject = () => require('test_fixtures/api/projects/get.json'); +export const getBranch = () => require('test_fixtures/api/projects/branches/get.json'); +export const getMergeRequests = () => require('test_fixtures/api/merge_requests/get.json'); +export const getRepositoryFiles = () => require('test_fixtures/projects_json/files.json'); +export const getPipelinesEmptyResponse = () => + require('test_fixtures/projects_json/pipelines_empty.json'); +export const getCommit = memoize(() => getBranch().commit); diff --git a/spec/frontend_integration/test_helpers/mock_server/graphql.js b/spec/frontend_integration/test_helpers/mock_server/graphql.js new file mode 100644 index 00000000000..6dcc4798378 --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/graphql.js @@ -0,0 +1,21 @@ +import { buildSchema, graphql } from 'graphql'; +import gitlabSchemaStr from '../../../../doc/api/graphql/reference/gitlab_schema.graphql'; + +const graphqlSchema = buildSchema(gitlabSchemaStr.loc.source.body); +const graphqlResolvers = { + project({ fullPath }, schema) { + const result = schema.projects.findBy({ path_with_namespace: fullPath }); + const userPermission = schema.db.userPermissions[0]; + + return { + ...result.attrs, + userPermissions: { + ...userPermission, + }, + }; + }, +}; + +// eslint-disable-next-line import/prefer-default-export +export const graphqlQuery = (query, variables, schema) => + graphql(graphqlSchema, query, graphqlResolvers, schema, variables); diff --git a/spec/frontend_integration/test_helpers/mock_server/index.js b/spec/frontend_integration/test_helpers/mock_server/index.js new file mode 100644 index 00000000000..b3979d05ea5 --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/index.js @@ -0,0 +1,45 @@ +import { Server, Model, RestSerializer } from 'miragejs'; +import { getProject, getBranch, getMergeRequests, getRepositoryFiles } from 'test_helpers/fixtures'; +import setupRoutes from './routes'; + +export const createMockServerOptions = () => ({ + models: { + project: Model, + branch: Model, + mergeRequest: Model, + file: Model, + userPermission: Model, + }, + serializers: { + application: RestSerializer.extend({ + root: false, + }), + }, + seeds(schema) { + schema.db.loadData({ + files: getRepositoryFiles().map(path => ({ path })), + projects: [getProject()], + branches: [getBranch()], + mergeRequests: getMergeRequests(), + userPermissions: [ + { + createMergeRequestIn: true, + readMergeRequest: true, + pushCode: true, + }, + ], + }); + }, + routes() { + this.namespace = ''; + this.urlPrefix = '/'; + + setupRoutes(this); + }, +}); + +export const createMockServer = () => { + const server = new Server(createMockServerOptions()); + + return server; +}; diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/404.js b/spec/frontend_integration/test_helpers/mock_server/routes/404.js new file mode 100644 index 00000000000..9e08016577b --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/routes/404.js @@ -0,0 +1,7 @@ +export default server => { + ['get', 'post', 'put', 'delete', 'patch'].forEach(method => { + server[method]('*', () => { + return new Response(404); + }); + }); +}; diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/ci.js b/spec/frontend_integration/test_helpers/mock_server/routes/ci.js new file mode 100644 index 00000000000..83951f09c56 --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/routes/ci.js @@ -0,0 +1,11 @@ +import { getPipelinesEmptyResponse } from 'test_helpers/fixtures'; + +export default server => { + server.get('*/commit/:id/pipelines', () => { + return getPipelinesEmptyResponse(); + }); + + server.get('/api/v4/projects/:id/runners', () => { + return []; + }); +}; diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/graphql.js b/spec/frontend_integration/test_helpers/mock_server/routes/graphql.js new file mode 100644 index 00000000000..ebb5415ba97 --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/routes/graphql.js @@ -0,0 +1,11 @@ +import { graphqlQuery } from '../graphql'; + +export default server => { + server.post('/api/graphql', (schema, request) => { + const batches = JSON.parse(request.requestBody); + + return Promise.all( + batches.map(({ query, variables }) => graphqlQuery(query, variables, schema)), + ); + }); +}; diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/index.js b/spec/frontend_integration/test_helpers/mock_server/routes/index.js new file mode 100644 index 00000000000..eea196b5158 --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/routes/index.js @@ -0,0 +1,12 @@ +/* eslint-disable global-require */ +export default server => { + [ + require('./graphql'), + require('./projects'), + require('./repository'), + require('./ci'), + require('./404'), + ].forEach(({ default: setup }) => { + setup(server); + }); +}; diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/projects.js b/spec/frontend_integration/test_helpers/mock_server/routes/projects.js new file mode 100644 index 00000000000..f4d8ce4b23d --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/routes/projects.js @@ -0,0 +1,23 @@ +import { withKeys } from 'test_helpers/utils/obj'; + +export default server => { + server.get('/api/v4/projects/:id', (schema, request) => { + const { id } = request.params; + + const proj = + schema.projects.findBy({ id }) ?? schema.projects.findBy({ path_with_namespace: id }); + + return proj.attrs; + }); + + server.get('/api/v4/projects/:id/merge_requests', (schema, request) => { + const result = schema.mergeRequests.where( + withKeys(request.queryParams, { + source_project_id: 'project_id', + source_branch: 'source_branch', + }), + ); + + return result.models; + }); +}; diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/repository.js b/spec/frontend_integration/test_helpers/mock_server/routes/repository.js new file mode 100644 index 00000000000..c5e91c9e87e --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/routes/repository.js @@ -0,0 +1,38 @@ +import { createNewCommit, createCommitIdGenerator } from 'test_helpers/factories'; + +export default server => { + const commitIdGenerator = createCommitIdGenerator(); + + server.get('/api/v4/projects/:id/repository/branches', schema => { + return schema.db.branches; + }); + + server.get('/api/v4/projects/:id/repository/branches/:name', (schema, request) => { + const { name } = request.params; + + const branch = schema.branches.findBy({ name }); + + return branch.attrs; + }); + + server.get('*/-/files/:id', schema => { + return schema.db.files.map(({ path }) => path); + }); + + server.post('/api/v4/projects/:id/repository/commits', (schema, request) => { + const { branch: branchName, commit_message: message, actions } = JSON.parse( + request.requestBody, + ); + + const branch = schema.branches.findBy({ name: branchName }); + + const commit = { + ...createNewCommit({ id: commitIdGenerator.next(), message }, branch.attrs.commit), + __actions: actions, + }; + + branch.update({ commit }); + + return commit; + }); +}; diff --git a/spec/frontend_integration/test_helpers/mock_server/use.js b/spec/frontend_integration/test_helpers/mock_server/use.js new file mode 100644 index 00000000000..84597d57584 --- /dev/null +++ b/spec/frontend_integration/test_helpers/mock_server/use.js @@ -0,0 +1,5 @@ +import { createMockServer } from './index'; + +if (process.env.NODE_ENV === 'development') { + window.mockServer = createMockServer(); +} diff --git a/spec/frontend_integration/test_helpers/setup/index.js b/spec/frontend_integration/test_helpers/setup/index.js new file mode 100644 index 00000000000..ba1d256e16e --- /dev/null +++ b/spec/frontend_integration/test_helpers/setup/index.js @@ -0,0 +1,5 @@ +import '../../../frontend/test_setup'; +import './setup_globals'; +import './setup_axios'; +import './setup_serializers'; +import './setup_mock_server'; diff --git a/spec/frontend_integration/test_helpers/setup/setup_axios.js b/spec/frontend_integration/test_helpers/setup/setup_axios.js new file mode 100644 index 00000000000..efdaf8016f2 --- /dev/null +++ b/spec/frontend_integration/test_helpers/setup/setup_axios.js @@ -0,0 +1,5 @@ +import adapter from 'axios/lib/adapters/xhr'; +import axios from '~/lib/utils/axios_utils'; + +// We're removing our default axios adapter because this is handled by our mock server now +axios.defaults.adapter = adapter; diff --git a/spec/frontend_integration/test_helpers/setup/setup_globals.js b/spec/frontend_integration/test_helpers/setup/setup_globals.js new file mode 100644 index 00000000000..2b0e8f76c3c --- /dev/null +++ b/spec/frontend_integration/test_helpers/setup/setup_globals.js @@ -0,0 +1,15 @@ +import { setTestTimeout } from 'helpers/timeout'; + +beforeEach(() => { + window.gon = { + api_version: 'v4', + relative_url_root: '', + }; + + setTestTimeout(5000); + jest.useRealTimers(); +}); + +afterEach(() => { + jest.useFakeTimers(); +}); diff --git a/spec/frontend_integration/test_helpers/setup/setup_mock_server.js b/spec/frontend_integration/test_helpers/setup/setup_mock_server.js new file mode 100644 index 00000000000..343aeebf88e --- /dev/null +++ b/spec/frontend_integration/test_helpers/setup/setup_mock_server.js @@ -0,0 +1,13 @@ +import { createMockServer } from '../mock_server'; + +beforeEach(() => { + const server = createMockServer(); + server.logging = false; + + global.mockServer = server; +}); + +afterEach(() => { + global.mockServer.shutdown(); + global.mockServer = null; +}); diff --git a/spec/frontend_integration/test_helpers/setup/setup_serializers.js b/spec/frontend_integration/test_helpers/setup/setup_serializers.js new file mode 100644 index 00000000000..6c1de853129 --- /dev/null +++ b/spec/frontend_integration/test_helpers/setup/setup_serializers.js @@ -0,0 +1,3 @@ +import defaultSerializer from '../snapshot_serializer'; + +expect.addSnapshotSerializer(defaultSerializer); diff --git a/spec/frontend_integration/test_helpers/snapshot_serializer.js b/spec/frontend_integration/test_helpers/snapshot_serializer.js new file mode 100644 index 00000000000..8c4f95a9156 --- /dev/null +++ b/spec/frontend_integration/test_helpers/snapshot_serializer.js @@ -0,0 +1,18 @@ +export default { + test(value) { + return value instanceof HTMLElement && !value.$_hit; + }, + print(element, serialize) { + element.$_hit = true; + element.querySelectorAll('[style]').forEach(el => { + el.$_hit = true; + if (el.style.display === 'none') { + el.textContent = '(jest: contents hidden)'; + } + }); + + return serialize(element) + .replace(/^\s*<!---->$/gm, '') + .replace(/\n\s*\n/gm, '\n'); + }, +}; diff --git a/spec/frontend_integration/test_helpers/utils/obj.js b/spec/frontend_integration/test_helpers/utils/obj.js new file mode 100644 index 00000000000..6c301798489 --- /dev/null +++ b/spec/frontend_integration/test_helpers/utils/obj.js @@ -0,0 +1,36 @@ +import { has, mapKeys, pick } from 'lodash'; + +/** + * This method is used to type-safely set values on the given object + * + * @template T + * @returns {T} A shallow copy of `obj`, with the values from `values` + * @throws {Error} If `values` contains a key that isn't already on `obj` + * @param {T} source + * @param {Object} values + */ +export const withValues = (source, values) => + Object.entries(values).reduce( + (acc, [key, value]) => { + if (!has(acc, key)) { + throw new Error( + `[mock_server] Cannot write property that does not exist on object '${key}'`, + ); + } + + return { + ...acc, + [key]: value, + }; + }, + { ...source }, + ); + +/** + * This method returns a subset of the given object and maps the key names based on the + * given `keys`. + * + * @param {Object} obj The source object. + * @param {Object} map The object which contains the keys to use and mapped key names. + */ +export const withKeys = (obj, map) => mapKeys(pick(obj, Object.keys(map)), (val, key) => map[key]); diff --git a/spec/frontend_integration/test_helpers/utils/obj_spec.js b/spec/frontend_integration/test_helpers/utils/obj_spec.js new file mode 100644 index 00000000000..0ad7b4a1a4c --- /dev/null +++ b/spec/frontend_integration/test_helpers/utils/obj_spec.js @@ -0,0 +1,23 @@ +import { withKeys, withValues } from './obj'; + +describe('frontend_integration/test_helpers/utils/obj', () => { + describe('withKeys', () => { + it('picks and maps keys', () => { + expect(withKeys({ a: '123', b: 456, c: 'd' }, { b: 'lorem', c: 'ipsum', z: 'zed ' })).toEqual( + { lorem: 456, ipsum: 'd' }, + ); + }); + }); + + describe('withValues', () => { + it('sets values', () => { + expect(withValues({ a: '123', b: 456 }, { b: 789 })).toEqual({ a: '123', b: 789 }); + }); + + it('throws if values has non-existent key', () => { + expect(() => withValues({ a: '123', b: 456 }, { b: 789, bogus: 'throws' })).toThrow( + `[mock_server] Cannot write property that does not exist on object 'bogus'`, + ); + }); + }); +}); diff --git a/spec/frontend_integration/test_helpers/utils/overclock_timers.js b/spec/frontend_integration/test_helpers/utils/overclock_timers.js new file mode 100644 index 00000000000..046c7f8e527 --- /dev/null +++ b/spec/frontend_integration/test_helpers/utils/overclock_timers.js @@ -0,0 +1,65 @@ +/** + * This function replaces the existing `setTimeout` and `setInterval` with wrappers that + * discount the `ms` passed in by `boost`. + * + * For example, if a module has: + * + * ``` + * setTimeout(cb, 100); + * ``` + * + * But a test has: + * + * ``` + * useOverclockTimers(25); + * ``` + * + * Then the module's call to `setTimeout` effectively becomes: + * + * ``` + * setTimeout(cb, 4); + * ``` + * + * It's important to note that the timing for `setTimeout` and order of execution is non-deterministic + * and discounting the `ms` passed could make this very obvious and expose some underlying issues + * with flaky failures. + * + * WARNING: If flaky spec failures show up in a spec that is using this helper, please consider either: + * + * - Refactoring the production code so that it's reactive to state changes, not dependent on timers. + * - Removing the call to this helper from the spec. + * + * @param {Number} boost + */ +// eslint-disable-next-line import/prefer-default-export +export const useOverclockTimers = (boost = 50) => { + if (boost <= 0) { + throw new Error(`[overclock_timers] boost (${boost}) cannot be <= 0`); + } + + let origSetTimeout; + let origSetInterval; + const newSetTimeout = (fn, msParam = 0) => { + const ms = msParam > 0 ? Math.floor(msParam / boost) : msParam; + + return origSetTimeout(fn, ms); + }; + const newSetInterval = (fn, msParam = 0) => { + const ms = msParam > 0 ? Math.floor(msParam / boost) : msParam; + + return origSetInterval(fn, ms); + }; + + beforeEach(() => { + origSetTimeout = global.setTimeout; + origSetInterval = global.setInterval; + + global.setTimeout = newSetTimeout; + global.setInterval = newSetInterval; + }); + + afterEach(() => { + global.setTimeout = origSetTimeout; + global.setInterval = origSetInterval; + }); +}; diff --git a/spec/frontend_integration/test_setup.js b/spec/frontend_integration/test_setup.js new file mode 100644 index 00000000000..8db22c56245 --- /dev/null +++ b/spec/frontend_integration/test_setup.js @@ -0,0 +1 @@ +import './test_helpers/setup'; |