mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 17:31:43 +00:00
Integration test updates (#12121)
This commit is contained in:
parent
52130ea0e9
commit
82f8b6d1d2
@ -15,11 +15,11 @@ import { memoize } from 'lodash'
|
||||
// NOTE borderline reasonable to define an interface here instead of duplicating
|
||||
const WEB_TSPROJECT_PATH = path.join(__dirname, '../../web')
|
||||
const WEB_OUTPUT_DIR = path.join(__dirname, '../../web/src')
|
||||
const WEB_INTERFACE_NAME = 'WebGQLOperations'
|
||||
const WEB_INTERFACE_NAME = 'WebGraphQlOperations'
|
||||
|
||||
const SHARED_TSPROJECT_PATH = path.join(__dirname, '../')
|
||||
const SHARED_OUTPUT_DIR = path.join(__dirname, '../src/')
|
||||
const SHARED_INTERFACE_NAME = 'SharedGQLOperations'
|
||||
const SHARED_INTERFACE_NAME = 'SharedGraphQlOperations'
|
||||
|
||||
const readSchema = (schemaPath: string): GraphQLSchema => {
|
||||
const isExists = fs.existsSync(schemaPath)
|
||||
@ -156,7 +156,7 @@ const extractGQL = async (tsProjectPath: string, outputDirectory: string, interf
|
||||
]
|
||||
|
||||
// TODO as an option
|
||||
const outputFileName = 'gql-operations.ts'
|
||||
const outputFileName = 'graphql-operations.ts'
|
||||
|
||||
const sourceFile = ts.createSourceFile(outputFileName, '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS)
|
||||
const resultFile = ts.updateSourceFileNode(sourceFile, typeDeclarations)
|
||||
@ -21,7 +21,7 @@
|
||||
"test": "jest",
|
||||
"graphql": "gulp graphQLTypes",
|
||||
"schema": "gulp schema",
|
||||
"gql-extract-operations": "TS_NODE_COMPILER_OPTIONS=\"{\\\"module\\\":\\\"commonjs\\\"}\" ts-node ./dev/extract-gql-tags.ts",
|
||||
"extract-graphql-operations": "TS_NODE_COMPILER_OPTIONS=\"{\\\"module\\\":\\\"commonjs\\\"}\" ts-node ./dev/extract-graphql-operations.ts",
|
||||
"watch-schema": "gulp watchSchema"
|
||||
},
|
||||
"sideEffects": true
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
||||
/* This is an autogenerated file. Do not edit this file directly! */
|
||||
export interface SharedGQLOperations {
|
||||
export interface SharedGraphQlOperations {
|
||||
ResolveRawRepoName: /* src/backend/repo.ts */ (variables: ResolveRawRepoNameVariables) => ResolveRawRepoNameResult
|
||||
Extensions: /* src/extensions/helpers.ts */ (variables: ExtensionsVariables) => ExtensionsResult
|
||||
EditSettings: /* src/settings/edit.ts */ (variables: EditSettingsVariables) => EditSettingsResult
|
||||
@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
||||
/* This is an autogenerated file. Do not edit this file directly! */
|
||||
export interface WebGQLOperations {
|
||||
export interface WebGraphQlOperations {
|
||||
CurrentAuthState: /* src/auth.ts */ (variables: CurrentAuthStateVariables) => CurrentAuthStateResult
|
||||
RepositoryID: /* src/enterprise/campaigns/detail/AddChangesetForm.tsx */ (
|
||||
variables: RepositoryIDVariables
|
||||
@ -9,7 +9,7 @@ import { Polly } from '@pollyjs/core'
|
||||
import { PuppeteerAdapter } from './polly/PuppeteerAdapter'
|
||||
import FSPersister from '@pollyjs/persister-fs'
|
||||
import { ErrorGraphQLResult, SuccessGraphQLResult } from '../../../shared/src/graphql/graphql'
|
||||
|
||||
import MockDate from 'mockdate'
|
||||
import { first, timeoutWith } from 'rxjs/operators'
|
||||
import * as path from 'path'
|
||||
import * as util from 'util'
|
||||
@ -18,10 +18,11 @@ import * as prettier from 'prettier'
|
||||
import html from 'tagged-template-noop'
|
||||
import { createJsContext } from './jscontext'
|
||||
import { SourcegraphContext } from '../jscontext'
|
||||
import { WebGQLOperations } from '../gql-operations'
|
||||
import { SharedGQLOperations } from '../../../shared/src/gql-operations'
|
||||
import { WebGraphQlOperations } from '../graphql-operations'
|
||||
import { SharedGraphQlOperations } from '../../../shared/src/graphql-operations'
|
||||
import { keyExistsIn } from '../../../shared/src/util/types'
|
||||
import { IGraphQLResponseError } from '../../../shared/src/graphql/schema'
|
||||
import { getConfig } from '../../../shared/src/testing/config'
|
||||
|
||||
// Reduce log verbosity
|
||||
util.inspect.defaultOptions.depth = 0
|
||||
@ -39,19 +40,15 @@ type IntegrationTestInitGeneration = () => Promise<{
|
||||
subscriptions?: Subscription
|
||||
}>
|
||||
|
||||
// type PotentialOverrides<T> = Partial<
|
||||
// { [K in keyof T]: T[K] extends (input: any) => infer Result ? Result | StubbedErrorResponse : never }
|
||||
// >
|
||||
type PotentialOverrides<T> = Partial<T>
|
||||
|
||||
export class IntegrationTestGqlError extends Error {
|
||||
export class IntegrationTestGraphQlError extends Error {
|
||||
constructor(public errors: IGraphQLResponseError[]) {
|
||||
super('graphql error for integration tests')
|
||||
}
|
||||
}
|
||||
|
||||
type AllGQLOperations = WebGQLOperations & SharedGQLOperations
|
||||
export type GraphQLOverrides = PotentialOverrides<AllGQLOperations>
|
||||
type AllGraphQlOperations = WebGraphQlOperations & SharedGraphQlOperations
|
||||
type GraphQLOperationName = keyof AllGraphQlOperations & string
|
||||
export type GraphQLOverrides = Partial<AllGraphQlOperations>
|
||||
|
||||
interface TestContext {
|
||||
sourcegraphBaseUrl: string
|
||||
@ -74,13 +71,13 @@ interface TestContext {
|
||||
* If the query does not happen within a few seconds, it throws a timeout error.
|
||||
*
|
||||
* @param triggerRequest A callback called to trigger the request (e.g. clicking a button). The request MUST be triggered within this callback.
|
||||
* @param queryName The name of the query to wait for.
|
||||
* @param operationName The name of the query to wait for.
|
||||
* @returns The GraphQL variables of the query.
|
||||
*/
|
||||
waitForGraphQLRequest: <Operation extends keyof AllGQLOperations & string>(
|
||||
waitForGraphQLRequest: <O extends GraphQLOperationName>(
|
||||
triggerRequest: () => Promise<void> | void,
|
||||
queryName: Operation
|
||||
) => Promise<AllGQLOperations[Operation] extends (input: infer InputVariables) => any ? InputVariables : never>
|
||||
operationName: O
|
||||
) => Promise<Parameters<AllGraphQlOperations[O]>[0]>
|
||||
}
|
||||
|
||||
type TestBody = (context: TestContext) => Promise<void>
|
||||
@ -184,43 +181,52 @@ export function describeIntegration(description: string, testSuite: IntegrationT
|
||||
server.get(new URL('/.assets/*path', sourcegraphBaseUrl).href).passthrough()
|
||||
|
||||
// GraphQL requests are not handled by HARs, but configured per-test.
|
||||
interface GraphQLRequestEvent<O extends GraphQLOperationName> {
|
||||
operationName: O
|
||||
variables: Parameters<AllGraphQlOperations[O]>[0]
|
||||
}
|
||||
let graphQlOverrides: GraphQLOverrides = commonGraphQlResults
|
||||
const graphQlRequests = new Subject<{ queryName: string; variables: unknown }>()
|
||||
const graphQlRequests = new Subject<GraphQLRequestEvent<GraphQLOperationName>>()
|
||||
server.post(new URL('/.api/graphql', sourcegraphBaseUrl).href).intercept((request, response) => {
|
||||
const queryName = new URL(request.absoluteUrl).search.slice(1)
|
||||
const { variables, query } = request.jsonBody() as { query: string; variables: object }
|
||||
graphQlRequests.next({ queryName, variables })
|
||||
if (!graphQlOverrides || !keyExistsIn(queryName, graphQlOverrides)) {
|
||||
// False positive
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
const operationName = new URL(request.absoluteUrl).search.slice(1) as GraphQLOperationName
|
||||
const { variables, query } = request.jsonBody() as {
|
||||
query: string
|
||||
variables: Parameters<AllGraphQlOperations[GraphQLOperationName]>[0]
|
||||
}
|
||||
graphQlRequests.next({ operationName, variables })
|
||||
|
||||
const missingOverrideError = (): Error => {
|
||||
const formattedQuery = prettier.format(query, { parser: 'graphql' }).trim()
|
||||
const formattedVariables = util.inspect(variables)
|
||||
const error = new Error(
|
||||
`GraphQL query "${queryName}" has no configured mock response. Make sure the call to overrideGraphQL() includes a result for the "${queryName}" query:\n${formattedVariables} ⤵️\n${formattedQuery}`
|
||||
`GraphQL query "${operationName}" has no configured mock response. Make sure the call to overrideGraphQL() includes a result for the "${operationName}" query:\n${formattedVariables} ⤵️\n${formattedQuery}`
|
||||
)
|
||||
// Make test fail
|
||||
errors.error(error)
|
||||
throw error
|
||||
return error
|
||||
}
|
||||
const handler = graphQlOverrides[queryName]
|
||||
|
||||
if (handler === undefined) {
|
||||
throw new Error('we technically check that above, so this error to make ts happy')
|
||||
if (!graphQlOverrides || !keyExistsIn(operationName, graphQlOverrides)) {
|
||||
throw missingOverrideError()
|
||||
}
|
||||
const handler = graphQlOverrides[operationName]
|
||||
if (!handler) {
|
||||
throw missingOverrideError()
|
||||
}
|
||||
|
||||
try {
|
||||
const result = handler(variables as any)
|
||||
const gqlResult: SuccessGraphQLResult<any> = { data: result, errors: undefined }
|
||||
response.json(gqlResult)
|
||||
const graphQlResult: SuccessGraphQLResult<any> = { data: result, errors: undefined }
|
||||
response.json(graphQlResult)
|
||||
} catch (error) {
|
||||
if (!(error instanceof IntegrationTestGqlError)) {
|
||||
const error = new Error(
|
||||
`GraphQL query "${queryName}" threw an exception but it was not IntegrationTestGqlError, please use 'throw new IntegrationTestGqlError()' instead`
|
||||
)
|
||||
if (!(error instanceof IntegrationTestGraphQlError)) {
|
||||
errors.error(error)
|
||||
throw error
|
||||
}
|
||||
|
||||
const gqlError: ErrorGraphQLResult = { data: undefined, errors: error.errors }
|
||||
response.json(gqlError)
|
||||
const graphQlError: ErrorGraphQLResult = { data: undefined, errors: error.errors }
|
||||
response.json(graphQlError)
|
||||
}
|
||||
})
|
||||
|
||||
@ -267,20 +273,29 @@ export function describeIntegration(description: string, testSuite: IntegrationT
|
||||
overrideJsContext: override => {
|
||||
jsContext = override
|
||||
},
|
||||
waitForGraphQLRequest: async (triggerRequest, queryName) => {
|
||||
waitForGraphQLRequest: async <O extends GraphQLOperationName>(
|
||||
triggerRequest: () => Promise<void> | void,
|
||||
operationName: O
|
||||
): Promise<Parameters<AllGraphQlOperations[O]>[0]> => {
|
||||
const requestPromise = graphQlRequests
|
||||
.pipe(
|
||||
first(request => request.queryName === queryName),
|
||||
first(
|
||||
(
|
||||
request: GraphQLRequestEvent<GraphQLOperationName>
|
||||
): request is GraphQLRequestEvent<O> =>
|
||||
request.operationName === operationName
|
||||
),
|
||||
timeoutWith(
|
||||
4000,
|
||||
throwError(new Error(`Timeout waiting for GraphQL request "${queryName}"`))
|
||||
throwError(
|
||||
new Error(`Timeout waiting for GraphQL request "${operationName}"`)
|
||||
)
|
||||
)
|
||||
)
|
||||
.toPromise()
|
||||
await triggerRequest()
|
||||
const { variables } = await requestPromise
|
||||
// trust type system to infer the right shape based on the usage
|
||||
return variables as ReturnType<TestContext['waitForGraphQLRequest']>
|
||||
return variables
|
||||
},
|
||||
}),
|
||||
])
|
||||
@ -319,7 +334,21 @@ export function describeIntegration(description: string, testSuite: IntegrationT
|
||||
})
|
||||
})
|
||||
}
|
||||
let initGenerationCallback: IntegrationTestInitGeneration | null = null
|
||||
let initGenerationCallback: IntegrationTestInitGeneration = async () => {
|
||||
// Reset date mocking
|
||||
MockDate.reset()
|
||||
const { sourcegraphBaseUrl, headless, slowMo } = getConfig('sourcegraphBaseUrl', 'headless', 'slowMo')
|
||||
|
||||
// Start browser
|
||||
const driver = await createDriverForTest({
|
||||
sourcegraphBaseUrl,
|
||||
logBrowserConsole: true,
|
||||
headless,
|
||||
slowMo,
|
||||
})
|
||||
return { driver, sourcegraphBaseUrl }
|
||||
}
|
||||
|
||||
const initGeneration = (logic: IntegrationTestInitGeneration): void => {
|
||||
initGenerationCallback = logic
|
||||
}
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
import { createDriverForTest } from '../../../shared/src/testing/driver'
|
||||
import MockDate from 'mockdate'
|
||||
import { getConfig } from '../../../shared/src/testing/config'
|
||||
import assert from 'assert'
|
||||
import expect from 'expect'
|
||||
import { describeIntegration } from './helpers'
|
||||
import { commonGraphQlResults } from './graphQlResults'
|
||||
import { ILanguage, IRepository, ISearchResultMatch } from '../../../shared/src/graphql/schema'
|
||||
import { SearchResult } from '../gql-operations'
|
||||
import { ILanguage, IRepository } from '../../../shared/src/graphql/schema'
|
||||
import { SearchResult } from '../graphql-operations'
|
||||
|
||||
const searchResults = (): SearchResult => ({
|
||||
search: {
|
||||
@ -15,10 +12,10 @@ const searchResults = (): SearchResult => ({
|
||||
limitHit: true,
|
||||
matchCount: 30,
|
||||
approximateResultCount: '30+',
|
||||
missing: [] as IRepository[],
|
||||
cloning: [] as IRepository[],
|
||||
missing: [],
|
||||
cloning: [],
|
||||
repositoriesCount: 372,
|
||||
timedout: [] as IRepository[],
|
||||
timedout: [],
|
||||
indexUnavailable: false,
|
||||
dynamicFilters: [
|
||||
{
|
||||
@ -56,7 +53,7 @@ const searchResults = (): SearchResult => ({
|
||||
icon:
|
||||
'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgNjQgNjQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDY0IDY0OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+Cjx0aXRsZT5JY29ucyA0MDA8L3RpdGxlPgo8Zz4KCTxwYXRoIGQ9Ik0yMywyMi40YzEuMywwLDIuNC0xLjEsMi40LTIuNHMtMS4xLTIuNC0yLjQtMi40Yy0xLjMsMC0yLjQsMS4xLTIuNCwyLjRTMjEuNywyMi40LDIzLDIyLjR6Ii8+Cgk8cGF0aCBkPSJNMzUsMjYuNGMxLjMsMCwyLjQtMS4xLDIuNC0yLjRzLTEuMS0yLjQtMi40LTIuNHMtMi40LDEuMS0yLjQsMi40UzMzLjcsMjYuNCwzNSwyNi40eiIvPgoJPHBhdGggZD0iTTIzLDQyLjRjMS4zLDAsMi40LTEuMSwyLjQtMi40cy0xLjEtMi40LTIuNC0yLjRzLTIuNCwxLjEtMi40LDIuNFMyMS43LDQyLjQsMjMsNDIuNHoiLz4KCTxwYXRoIGQ9Ik01MCwxNmgtMS41Yy0wLjMsMC0wLjUsMC4yLTAuNSwwLjV2MzVjMCwwLjMtMC4yLDAuNS0wLjUsMC41aC0yN2MtMC41LDAtMS0wLjItMS40LTAuNmwtMC42LTAuNmMtMC4xLTAuMS0wLjEtMC4yLTAuMS0wLjQKCQljMC0wLjMsMC4yLTAuNSwwLjUtMC41SDQ0YzEuMSwwLDItMC45LDItMlYxMmMwLTEuMS0wLjktMi0yLTJIMTRjLTEuMSwwLTIsMC45LTIsMnYzNi4zYzAsMS4xLDAuNCwyLjEsMS4yLDIuOGwzLjEsMy4xCgkJYzEuMSwxLjEsMi43LDEuOCw0LjIsMS44SDUwYzEuMSwwLDItMC45LDItMlYxOEM1MiwxNi45LDUxLjEsMTYsNTAsMTZ6IE0xOSwyMGMwLTIuMiwxLjgtNCw0LTRjMS40LDAsMi44LDAuOCwzLjUsMgoJCWMxLjEsMS45LDAuNCw0LjMtMS41LDUuNFYzM2MxLTAuNiwyLjMtMC45LDQtMC45YzEsMCwyLTAuNSwyLjgtMS4zQzMyLjUsMzAsMzMsMjkuMSwzMywyOHYtMC42Yy0xLjItMC43LTItMi0yLTMuNQoJCWMwLTIuMiwxLjgtNCw0LTRjMi4yLDAsNCwxLjgsNCw0YzAsMS41LTAuOCwyLjctMiwzLjVoMGMtMC4xLDIuMS0wLjksNC40LTIuNSw2Yy0xLjYsMS42LTMuNCwyLjQtNS41LDIuNWMtMC44LDAtMS40LDAuMS0xLjksMC4zCgkJYy0wLjIsMC4xLTEsMC44LTEuMiwwLjlDMjYuNiwzOCwyNywzOC45LDI3LDQwYzAsMi4yLTEuOCw0LTQsNHMtNC0xLjgtNC00YzAtMS41LDAuOC0yLjcsMi0zLjRWMjMuNEMxOS44LDIyLjcsMTksMjEuNCwxOSwyMHoiLz4KPC9nPgo8L3N2Zz4K',
|
||||
detail: { html: '\u003Cp\u003ERepository name match\u003C/p\u003E\n' },
|
||||
matches: [] as ISearchResultMatch[],
|
||||
matches: [],
|
||||
},
|
||||
],
|
||||
alert: null,
|
||||
@ -65,28 +62,7 @@ const searchResults = (): SearchResult => ({
|
||||
},
|
||||
})
|
||||
|
||||
describeIntegration('Search', ({ initGeneration, describe }) => {
|
||||
initGeneration(async () => {
|
||||
// Reset date mocking
|
||||
MockDate.reset()
|
||||
const { sourcegraphBaseUrl, headless, slowMo, testUserPassword } = getConfig(
|
||||
'sourcegraphBaseUrl',
|
||||
'headless',
|
||||
'slowMo',
|
||||
'testUserPassword'
|
||||
)
|
||||
|
||||
// Start browser
|
||||
const driver = await createDriverForTest({
|
||||
sourcegraphBaseUrl,
|
||||
logBrowserConsole: true,
|
||||
headless,
|
||||
slowMo,
|
||||
})
|
||||
await driver.ensureLoggedIn({ username: 'test', password: testUserPassword, email: 'test@test.com' })
|
||||
return { driver, sourcegraphBaseUrl }
|
||||
})
|
||||
|
||||
describeIntegration('Search', ({ describe }) => {
|
||||
describe('Interactive search mode', ({ test }) => {
|
||||
test('Search mode component appears', async ({ sourcegraphBaseUrl, driver }) => {
|
||||
await driver.page.goto(sourcegraphBaseUrl + '/search')
|
||||
@ -102,6 +78,7 @@ describeIntegration('Search', ({ initGeneration, describe }) => {
|
||||
SearchSuggestions: () => ({
|
||||
search: {
|
||||
suggestions: [
|
||||
// TODO the type generation is not correct for this query which causes the need for these casts
|
||||
{ __typename: 'Language' } as ILanguage,
|
||||
{ __typename: 'Repository', name: 'github.com/gorilla/mux' } as IRepository,
|
||||
{ __typename: 'Repository', name: 'github.com/gorilla/css' } as IRepository,
|
||||
|
||||
@ -1,34 +1,9 @@
|
||||
import { createDriverForTest } from '../../../shared/src/testing/driver'
|
||||
import MockDate from 'mockdate'
|
||||
import { getConfig } from '../../../shared/src/testing/config'
|
||||
import assert from 'assert'
|
||||
import { IOrgConnection, IUserEmail, IOrg } from '../../../shared/src/graphql/schema'
|
||||
import { describeIntegration } from './helpers'
|
||||
import { retry } from '../../../shared/src/testing/utils'
|
||||
import { commonGraphQlResults, testUserID, settingsID } from './graphQlResults'
|
||||
|
||||
describeIntegration('Settings', ({ initGeneration, describe }) => {
|
||||
initGeneration(async () => {
|
||||
// Reset date mocking
|
||||
MockDate.reset()
|
||||
const { sourcegraphBaseUrl, headless, slowMo, testUserPassword } = getConfig(
|
||||
'sourcegraphBaseUrl',
|
||||
'headless',
|
||||
'slowMo',
|
||||
'testUserPassword'
|
||||
)
|
||||
|
||||
// Start browser
|
||||
const driver = await createDriverForTest({
|
||||
sourcegraphBaseUrl,
|
||||
logBrowserConsole: true,
|
||||
headless,
|
||||
slowMo,
|
||||
})
|
||||
await driver.ensureLoggedIn({ username: 'test', password: testUserPassword, email: 'test@test.com' })
|
||||
return { driver, sourcegraphBaseUrl }
|
||||
})
|
||||
|
||||
describeIntegration('Settings', ({ describe }) => {
|
||||
describe('User settings page', ({ it }) => {
|
||||
it('updates user settings', async ({ driver, sourcegraphBaseUrl, overrideGraphQL, waitForGraphQLRequest }) => {
|
||||
overrideGraphQL({
|
||||
@ -69,8 +44,8 @@ describeIntegration('Settings', ({ initGeneration, describe }) => {
|
||||
siteAdmin: true,
|
||||
builtinAuth: true,
|
||||
createdAt: '2020-03-02T11:52:15Z',
|
||||
emails: [{ email: 'test@sourcegraph.test', verified: true } as IUserEmail],
|
||||
organizations: { nodes: [] as IOrg[] } as IOrgConnection,
|
||||
emails: [{ email: 'test@sourcegraph.test', verified: true }],
|
||||
organizations: { nodes: [] },
|
||||
permissionsInfo: null,
|
||||
},
|
||||
}),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user