Integration test updates (#12121)

This commit is contained in:
Felix Becker 2020-07-13 21:10:30 +02:00 committed by GitHub
parent 52130ea0e9
commit 82f8b6d1d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 86 additions and 105 deletions

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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,

View File

@ -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,
},
}),