mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 15:51:43 +00:00
Use mocha directly for integration tests and move to shared/ (#12130)
This commit is contained in:
parent
82f8b6d1d2
commit
a1d87dc56f
@ -129,6 +129,7 @@
|
||||
"@types/jsdom": "12.2.4",
|
||||
"@types/lodash": "4.14.157",
|
||||
"@types/marked": "1.1.0",
|
||||
"@types/mime-types": "2.1.0",
|
||||
"@types/mini-css-extract-plugin": "0.9.1",
|
||||
"@types/mkdirp-promise": "5.0.0",
|
||||
"@types/mocha": "7.0.2",
|
||||
@ -208,6 +209,7 @@
|
||||
"latest-version": "^5.1.0",
|
||||
"license-checker": "^25.0.1",
|
||||
"message-port-polyfill": "^0.2.0",
|
||||
"mime-types": "^2.1.27",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"mkdirp-promise": "^5.0.1",
|
||||
"mocha": "^7.2.0",
|
||||
|
||||
@ -28,6 +28,7 @@ import getFreePort from 'get-port'
|
||||
import puppeteerFirefox from 'puppeteer-firefox'
|
||||
import webExt from 'web-ext'
|
||||
import { isDefined } from '../util/types'
|
||||
import { getConfig } from './config'
|
||||
|
||||
/**
|
||||
* Returns a Promise for the next emission of the given event on the given Puppeteer page.
|
||||
@ -685,15 +686,17 @@ interface DriverOptions extends LaunchOptions {
|
||||
|
||||
sourcegraphBaseUrl: string
|
||||
|
||||
/** If true, print browser console messages to stdout. */
|
||||
/** If not `false`, print browser console messages to stdout. */
|
||||
logBrowserConsole?: boolean
|
||||
|
||||
/** If true, keep browser open when driver is closed */
|
||||
keepBrowser?: boolean
|
||||
}
|
||||
|
||||
export async function createDriverForTest(options: DriverOptions): Promise<Driver> {
|
||||
const { loadExtension, sourcegraphBaseUrl, logBrowserConsole, keepBrowser } = options
|
||||
export async function createDriverForTest(
|
||||
options: DriverOptions = getConfig('sourcegraphBaseUrl', 'headless', 'slowMo')
|
||||
): Promise<Driver> {
|
||||
const { loadExtension, sourcegraphBaseUrl, logBrowserConsole = true, keepBrowser } = options
|
||||
const args: string[] = []
|
||||
const launchOptions: puppeteer.LaunchOptions = {
|
||||
...options,
|
||||
|
||||
19
shared/src/testing/integration/README.md
Normal file
19
shared/src/testing/integration/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Integration tests
|
||||
|
||||
This file contains integration tests for the Sourcegraph UI.
|
||||
|
||||
The role of these integration tests is to provide in-browser testing of complex UI flows in isolation from the Sourcegraph backend. They are built on top of Puppeteer & Mocha.
|
||||
|
||||
These tests are declared using the helpers declared in [`helpers.ts`](./helpers.ts), which wrap Mocha helpers and provide a similar API.
|
||||
|
||||
Mocked data for the tests is generated by running them with a real backend, with the `RECORD` environment variable set to a truthy value:
|
||||
|
||||
```
|
||||
env RECORD=true yarn test-integration
|
||||
```
|
||||
|
||||
Responses will be intercepted and saved as JSON in the `__fixtures__` directory.
|
||||
|
||||
When running the tests, static JS/CSS assets will be served from the `ui/assets` directory. Other requests will be intercepted, and responded to using fixture data.
|
||||
|
||||
Before running tests, call `yarn build` to make sure to have up-to-date assets.
|
||||
234
shared/src/testing/integration/context.ts
Normal file
234
shared/src/testing/integration/context.ts
Normal file
@ -0,0 +1,234 @@
|
||||
import { Test } from 'mocha'
|
||||
import { Subject, throwError } from 'rxjs'
|
||||
import { snakeCase } from 'lodash'
|
||||
import { Driver } from '../driver'
|
||||
import { recordCoverage } from '../coverage'
|
||||
import { readFile } from 'mz/fs'
|
||||
import mkdirp from 'mkdirp-promise'
|
||||
import { Polly, PollyServer } from '@pollyjs/core'
|
||||
import { PuppeteerAdapter } from './polly/PuppeteerAdapter'
|
||||
import FSPersister from '@pollyjs/persister-fs'
|
||||
import { ErrorGraphQLResult, SuccessGraphQLResult } from '../../graphql/graphql'
|
||||
import { first, timeoutWith } from 'rxjs/operators'
|
||||
import * as path from 'path'
|
||||
import * as util from 'util'
|
||||
import * as prettier from 'prettier'
|
||||
import { keyExistsIn } from '../../util/types'
|
||||
import { IGraphQLResponseError } from '../../graphql/schema'
|
||||
import { readEnvironmentBoolean } from '../utils'
|
||||
import { ResourceType } from 'puppeteer'
|
||||
import * as mime from 'mime-types'
|
||||
|
||||
// Reduce log verbosity
|
||||
util.inspect.defaultOptions.depth = 0
|
||||
util.inspect.defaultOptions.maxStringLength = 80
|
||||
|
||||
Polly.register(PuppeteerAdapter as any)
|
||||
Polly.register(FSPersister)
|
||||
|
||||
const ASSETS_DIRECTORY = path.resolve(__dirname, '../../../../ui/assets')
|
||||
|
||||
const record = readEnvironmentBoolean({ variable: 'RECORD', defaultValue: false })
|
||||
|
||||
export class IntegrationTestGraphQlError extends Error {
|
||||
constructor(public errors: IGraphQLResponseError[]) {
|
||||
super('graphql error for integration tests')
|
||||
}
|
||||
}
|
||||
|
||||
export interface IntegrationTestContext<
|
||||
TGraphQlOperations extends Record<TGraphQlOperationNames, (variables: any) => any>,
|
||||
TGraphQlOperationNames extends string
|
||||
> {
|
||||
driver: Driver
|
||||
server: PollyServer
|
||||
|
||||
/**
|
||||
* Configures fake responses for GraphQL queries and mutations.
|
||||
*
|
||||
* @param overrides The results to return, keyed by query name.
|
||||
*/
|
||||
overrideGraphQL: (overrides: Partial<TGraphQlOperations>) => void
|
||||
|
||||
/**
|
||||
* Waits for a specific GraphQL query to happen and returns the variables passed to the request.
|
||||
* 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 operationName The name of the query to wait for.
|
||||
* @returns The GraphQL variables of the query.
|
||||
*/
|
||||
waitForGraphQLRequest: <O extends TGraphQlOperationNames>(
|
||||
triggerRequest: () => Promise<void> | void,
|
||||
operationName: O
|
||||
) => Promise<Parameters<TGraphQlOperations[O]>[0]>
|
||||
|
||||
dispose: () => Promise<void>
|
||||
}
|
||||
|
||||
export interface IntegrationTestOptions {
|
||||
/**
|
||||
* The test driver created in a `before()` hook.
|
||||
*/
|
||||
driver: Driver
|
||||
|
||||
/**
|
||||
* The value of `this.currentTest` in the `beforeEach()` hook.
|
||||
* Make sure the hook function is not an arrow function to access it.
|
||||
*/
|
||||
currentTest: Test
|
||||
|
||||
/**
|
||||
* The directory (value of `__dirname`) of the test file.
|
||||
*/
|
||||
directory: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called in a `beforeEach()` and saved into a local variable.
|
||||
*/
|
||||
export const createSharedIntegrationTestContext = async <
|
||||
TGraphQlOperations extends Record<TGraphQlOperationNames, (variables: any) => any>,
|
||||
TGraphQlOperationNames extends string
|
||||
>({
|
||||
driver,
|
||||
currentTest,
|
||||
directory,
|
||||
}: IntegrationTestOptions): Promise<IntegrationTestContext<TGraphQlOperations, TGraphQlOperationNames>> => {
|
||||
await driver.newPage()
|
||||
await driver.page.setRequestInterception(true)
|
||||
const recordingsDirectory = path.join(directory, '__fixtures__', snakeCase(currentTest.fullTitle()))
|
||||
if (record) {
|
||||
await mkdirp(recordingsDirectory)
|
||||
}
|
||||
const requestResourceTypes: ResourceType[] = ['xhr', 'fetch', 'document', 'script', 'stylesheet', 'image', 'font']
|
||||
const polly = new Polly(snakeCase(currentTest.title), {
|
||||
adapters: ['puppeteer'],
|
||||
adapterOptions: {
|
||||
puppeteer: {
|
||||
page: driver.page,
|
||||
requestResourceTypes,
|
||||
},
|
||||
},
|
||||
persister: 'fs',
|
||||
persisterOptions: {
|
||||
fs: {
|
||||
recordingsDir: recordingsDirectory,
|
||||
},
|
||||
},
|
||||
expiryStrategy: 'warn',
|
||||
recordIfMissing: record,
|
||||
matchRequestsBy: {
|
||||
method: true,
|
||||
body: true,
|
||||
order: true,
|
||||
// Origin header will change when running against a test instance
|
||||
headers: false,
|
||||
},
|
||||
mode: record ? 'record' : 'replay',
|
||||
logging: false,
|
||||
})
|
||||
const { server } = polly
|
||||
|
||||
// Let browser handle data: URIs
|
||||
server.get('data:*rest').passthrough()
|
||||
|
||||
// Serve assets from disk
|
||||
server.get(new URL('/.assets/*path', driver.sourcegraphBaseUrl).href).intercept(async (request, response) => {
|
||||
const asset = request.params.path
|
||||
const content = await readFile(path.join(ASSETS_DIRECTORY, asset), {
|
||||
// Polly doesn't support Buffers or streams at the moment
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
const contentType = mime.contentType(path.basename(asset))
|
||||
if (contentType) {
|
||||
response.type(contentType)
|
||||
}
|
||||
// Cache all responses for the entire lifetime of the test run
|
||||
response.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
|
||||
response.send(content)
|
||||
})
|
||||
|
||||
// GraphQL requests are not handled by HARs, but configured per-test.
|
||||
interface GraphQLRequestEvent<O extends TGraphQlOperationNames> {
|
||||
operationName: O
|
||||
variables: Parameters<TGraphQlOperations[O]>[0]
|
||||
}
|
||||
let graphQlOverrides: Partial<TGraphQlOperations> = {}
|
||||
const graphQlRequests = new Subject<GraphQLRequestEvent<TGraphQlOperationNames>>()
|
||||
server.post(new URL('/.api/graphql', driver.sourcegraphBaseUrl).href).intercept((request, response) => {
|
||||
const operationName = new URL(request.absoluteUrl).search.slice(1) as TGraphQlOperationNames
|
||||
const { variables, query } = request.jsonBody() as {
|
||||
query: string
|
||||
variables: Parameters<TGraphQlOperations[TGraphQlOperationNames]>[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 "${operationName}" has no configured mock response. Make sure the call to overrideGraphQL() includes a result for the "${operationName}" query:\n${formattedVariables} ⤵️\n${formattedQuery}`
|
||||
)
|
||||
return error
|
||||
}
|
||||
if (!graphQlOverrides || !keyExistsIn(operationName, graphQlOverrides)) {
|
||||
throw missingOverrideError()
|
||||
}
|
||||
const handler = graphQlOverrides[operationName]
|
||||
if (!handler) {
|
||||
throw missingOverrideError()
|
||||
}
|
||||
|
||||
try {
|
||||
const result = handler(variables as any)
|
||||
const graphQlResult: SuccessGraphQLResult<any> = { data: result, errors: undefined }
|
||||
response.json(graphQlResult)
|
||||
} catch (error) {
|
||||
if (!(error instanceof IntegrationTestGraphQlError)) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const graphQlError: ErrorGraphQLResult = { data: undefined, errors: error.errors }
|
||||
response.json(graphQlError)
|
||||
}
|
||||
})
|
||||
|
||||
// Filter out 'server' header filled in by Caddy before persisting responses,
|
||||
// otherwise tests will hang when replayed from recordings.
|
||||
server
|
||||
.any()
|
||||
.on('beforePersist', (request, recording: { response: { headers: { name: string; value: string }[] } }) => {
|
||||
recording.response.headers = recording.response.headers.filter(({ name }) => name !== 'server')
|
||||
})
|
||||
|
||||
return {
|
||||
driver,
|
||||
server,
|
||||
overrideGraphQL: overrides => {
|
||||
graphQlOverrides = overrides
|
||||
},
|
||||
waitForGraphQLRequest: async <O extends TGraphQlOperationNames>(
|
||||
triggerRequest: () => Promise<void> | void,
|
||||
operationName: O
|
||||
): Promise<Parameters<TGraphQlOperations[O]>[0]> => {
|
||||
const requestPromise = graphQlRequests
|
||||
.pipe(
|
||||
first(
|
||||
(request: GraphQLRequestEvent<TGraphQlOperationNames>): request is GraphQLRequestEvent<O> =>
|
||||
request.operationName === operationName
|
||||
),
|
||||
timeoutWith(4000, throwError(new Error(`Timeout waiting for GraphQL request "${operationName}"`)))
|
||||
)
|
||||
.toPromise()
|
||||
await triggerRequest()
|
||||
const { variables } = await requestPromise
|
||||
return variables
|
||||
},
|
||||
dispose: async () => {
|
||||
await polly.stop()
|
||||
await recordCoverage(driver.browser)
|
||||
await driver.page.close()
|
||||
},
|
||||
}
|
||||
}
|
||||
9
shared/src/testing/integration/graphQlResults.ts
Normal file
9
shared/src/testing/integration/graphQlResults.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { SharedGraphQlOperations } from '../../graphql-operations'
|
||||
|
||||
export const testUserID = 'TestUserID'
|
||||
export const settingsID = 123
|
||||
|
||||
/**
|
||||
* Predefined results for GraphQL requests that are made on almost every page.
|
||||
*/
|
||||
export const sharedGraphQlResults: Partial<SharedGraphQlOperations> = {}
|
||||
@ -1,8 +1,7 @@
|
||||
import { Polly, Request as PollyRequest } from '@pollyjs/core'
|
||||
import Puppeteer from 'puppeteer'
|
||||
import type * as Puppeteer from 'puppeteer'
|
||||
import PollyAdapter from '@pollyjs/adapter'
|
||||
import { Subscription, fromEvent } from 'rxjs'
|
||||
import puppeteer from '../../../../shared/src/types/puppeteer-firefox'
|
||||
|
||||
interface Response {
|
||||
statusCode: number
|
||||
@ -44,7 +43,7 @@ export class PuppeteerAdapter extends PollyAdapter {
|
||||
* A map of all intercepted requests to their respond function, which will be called by the
|
||||
* 'response' event listener, causing Polly to record the response content.
|
||||
*/
|
||||
private pendingRequests = new Map<puppeteer.Request, { respond: (response: Puppeteer.Response) => void }>()
|
||||
private pendingRequests = new Map<Puppeteer.Request, { respond: (response: Puppeteer.Response) => void }>()
|
||||
|
||||
/**
|
||||
* Maps passthrough requests to an object containing:
|
||||
@ -132,8 +131,8 @@ export class PuppeteerAdapter extends PollyAdapter {
|
||||
const {
|
||||
requestArguments: { request },
|
||||
} = (pollyRequest as unknown) as PollyRequestArguments
|
||||
let respond: (response: puppeteer.Response) => void
|
||||
const responsePromise = new Promise<puppeteer.Response>(resolve => (respond = resolve))
|
||||
let respond: (response: Puppeteer.Response) => void
|
||||
const responsePromise = new Promise<Puppeteer.Response>(resolve => (respond = resolve))
|
||||
this.passThroughRequests.set(request, { respond: response => respond(response), responsePromise })
|
||||
await request.continue()
|
||||
const response = await responsePromise
|
||||
64
web/src/integration/context.ts
Normal file
64
web/src/integration/context.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import {
|
||||
createSharedIntegrationTestContext,
|
||||
IntegrationTestContext,
|
||||
IntegrationTestOptions,
|
||||
} from '../../../shared/src/testing/integration/context'
|
||||
import { createJsContext } from './jscontext'
|
||||
import { SourcegraphContext } from '../jscontext'
|
||||
import { WebGraphQlOperations } from '../graphql-operations'
|
||||
import { SharedGraphQlOperations } from '../../../shared/src/graphql-operations'
|
||||
import html from 'tagged-template-noop'
|
||||
import { commonWebGraphQlResults } from './graphQlResults'
|
||||
|
||||
export interface WebIntegrationTestContext
|
||||
extends IntegrationTestContext<
|
||||
WebGraphQlOperations & SharedGraphQlOperations,
|
||||
string & keyof (WebGraphQlOperations & SharedGraphQlOperations)
|
||||
> {
|
||||
/**
|
||||
* Overrides `window.context` from the default created by `createJsContext()`.
|
||||
*/
|
||||
overrideJsContext: (jsContext: SourcegraphContext) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the intergation test context for integration tests testing the web app.
|
||||
* This should be called in a `beforeEach()` hook and assigned to a variable `testContext` in the test scope.
|
||||
*/
|
||||
export const createWebIntegrationTestContext = async ({
|
||||
driver,
|
||||
currentTest,
|
||||
directory,
|
||||
}: IntegrationTestOptions): Promise<WebIntegrationTestContext> => {
|
||||
const sharedTestContext = await createSharedIntegrationTestContext<
|
||||
WebGraphQlOperations & SharedGraphQlOperations,
|
||||
string & keyof (WebGraphQlOperations & SharedGraphQlOperations)
|
||||
>({ driver, currentTest, directory })
|
||||
sharedTestContext.overrideGraphQL(commonWebGraphQlResults)
|
||||
|
||||
// Serve all requests for index.html (everything that does not match the handlers above) the same index.html
|
||||
let jsContext = createJsContext({ sourcegraphBaseUrl: sharedTestContext.driver.sourcegraphBaseUrl })
|
||||
sharedTestContext.server.get(new URL('/*path', driver.sourcegraphBaseUrl).href).intercept((request, response) => {
|
||||
response.type('text/html').send(html`
|
||||
<html>
|
||||
<head>
|
||||
<title>Sourcegraph Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script>
|
||||
window.context = ${JSON.stringify(jsContext)}
|
||||
</script>
|
||||
<script src="/.assets/scripts/app.bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
})
|
||||
|
||||
return {
|
||||
...sharedTestContext,
|
||||
overrideJsContext: overrides => {
|
||||
jsContext = overrides
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,14 @@
|
||||
import { GraphQLOverrides } from './helpers'
|
||||
import { StatusMessage, IOrg, IAlert } from '../../../shared/src/graphql/schema'
|
||||
import { builtinAuthProvider, siteID, siteGQLID } from './jscontext'
|
||||
|
||||
export const testUserID = 'TestUserID'
|
||||
export const settingsID = 123
|
||||
import { StatusMessage } from '../../../shared/src/graphql/schema'
|
||||
import { builtinAuthProvider, siteGQLID, siteID } from './jscontext'
|
||||
import { WebGraphQlOperations } from '../graphql-operations'
|
||||
import { SharedGraphQlOperations } from '../../../shared/src/graphql-operations'
|
||||
import { testUserID, sharedGraphQlResults } from '../../../shared/src/testing/integration/graphQlResults'
|
||||
|
||||
/**
|
||||
* Predefined results for GraphQL requests that are made on almost every page.
|
||||
*/
|
||||
export const commonGraphQlResults: GraphQLOverrides = {
|
||||
export const commonWebGraphQlResults: Partial<WebGraphQlOperations & SharedGraphQlOperations> = {
|
||||
...sharedGraphQlResults,
|
||||
CurrentAuthState: () => ({
|
||||
currentUser: {
|
||||
__typename: 'User',
|
||||
@ -19,10 +19,10 @@ export const commonGraphQlResults: GraphQLOverrides = {
|
||||
email: 'felix@sourcegraph.com',
|
||||
displayName: null,
|
||||
siteAdmin: true,
|
||||
tags: [] as string[],
|
||||
tags: [],
|
||||
url: '/users/test',
|
||||
settingsURL: '/users/test/settings',
|
||||
organizations: { nodes: [] as IOrg[] },
|
||||
organizations: { nodes: [] },
|
||||
session: { canSignOut: true },
|
||||
viewerCanAdminister: true,
|
||||
},
|
||||
@ -56,7 +56,7 @@ export const commonGraphQlResults: GraphQLOverrides = {
|
||||
site: {
|
||||
needsRepositoryConfiguration: false,
|
||||
freeUsersExceeded: false,
|
||||
alerts: [] as IAlert[],
|
||||
alerts: [],
|
||||
authProviders: {
|
||||
nodes: [builtinAuthProvider],
|
||||
},
|
||||
|
||||
@ -1,386 +0,0 @@
|
||||
import { describe as mochaDescribe, test as mochaTest, before as mochaBefore } from 'mocha'
|
||||
import { Subscription, Subject, throwError } from 'rxjs'
|
||||
import { snakeCase } from 'lodash'
|
||||
import { createDriverForTest, Driver } from '../../../shared/src/testing/driver'
|
||||
import { recordCoverage } from '../../../shared/src/testing/coverage'
|
||||
import mkdirp from 'mkdirp-promise'
|
||||
import express from 'express'
|
||||
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'
|
||||
import { commonGraphQlResults } from './graphQlResults'
|
||||
import * as prettier from 'prettier'
|
||||
import html from 'tagged-template-noop'
|
||||
import { createJsContext } from './jscontext'
|
||||
import { SourcegraphContext } from '../jscontext'
|
||||
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
|
||||
util.inspect.defaultOptions.maxStringLength = 80
|
||||
|
||||
Polly.register(PuppeteerAdapter as any)
|
||||
Polly.register(FSPersister)
|
||||
|
||||
const FIXTURES_DIRECTORY = `${__dirname}/__fixtures__`
|
||||
const ASSETS_DIRECTORY = `${__dirname}/../../../ui/assets`
|
||||
|
||||
type IntegrationTestInitGeneration = () => Promise<{
|
||||
driver: Driver
|
||||
sourcegraphBaseUrl: string
|
||||
subscriptions?: Subscription
|
||||
}>
|
||||
|
||||
export class IntegrationTestGraphQlError extends Error {
|
||||
constructor(public errors: IGraphQLResponseError[]) {
|
||||
super('graphql error for integration tests')
|
||||
}
|
||||
}
|
||||
|
||||
type AllGraphQlOperations = WebGraphQlOperations & SharedGraphQlOperations
|
||||
type GraphQLOperationName = keyof AllGraphQlOperations & string
|
||||
export type GraphQLOverrides = Partial<AllGraphQlOperations>
|
||||
|
||||
interface TestContext {
|
||||
sourcegraphBaseUrl: string
|
||||
driver: Driver
|
||||
|
||||
/**
|
||||
* Configures fake responses for GraphQL queries and mutations.
|
||||
*
|
||||
* @param overrides The results to return, keyed by query name.
|
||||
*/
|
||||
overrideGraphQL: (overrides: GraphQLOverrides) => void
|
||||
|
||||
/**
|
||||
* Overrides `window.context` from the default created by `createJsContext()`.
|
||||
*/
|
||||
overrideJsContext: (jsContext: SourcegraphContext) => void
|
||||
|
||||
/**
|
||||
* Waits for a specific GraphQL query to happen and returns the variables passed to the request.
|
||||
* 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 operationName The name of the query to wait for.
|
||||
* @returns The GraphQL variables of the query.
|
||||
*/
|
||||
waitForGraphQLRequest: <O extends GraphQLOperationName>(
|
||||
triggerRequest: () => Promise<void> | void,
|
||||
operationName: O
|
||||
) => Promise<Parameters<AllGraphQlOperations[O]>[0]>
|
||||
}
|
||||
|
||||
type TestBody = (context: TestContext) => Promise<void>
|
||||
|
||||
interface IntegrationTestDefiner {
|
||||
(title: string, run: TestBody): void
|
||||
only: (title: string, run: TestBody) => void
|
||||
skip: (title: string, run?: TestBody) => void
|
||||
}
|
||||
|
||||
type IntegrationTestBeforeGeneration = (
|
||||
setupLogic: (options: { sourcegraphBaseUrl: string; driver: Driver }) => Promise<void>
|
||||
) => void
|
||||
|
||||
type IntegrationTestDescriber = (
|
||||
title: string,
|
||||
suite: (helpers: {
|
||||
before: IntegrationTestBeforeGeneration
|
||||
test: IntegrationTestDefiner
|
||||
it: IntegrationTestDefiner
|
||||
describe: IntegrationTestDescriber
|
||||
}) => void
|
||||
) => void
|
||||
|
||||
type IntegrationTestSuite = (helpers: {
|
||||
/**
|
||||
* Registers a calback that will be run before test data is generated,
|
||||
* responsible for creating the {@link Driver} and providing the Sourcegraph URL.
|
||||
*/
|
||||
initGeneration: (setupLogic: IntegrationTestInitGeneration) => void
|
||||
test: IntegrationTestDefiner
|
||||
describe: IntegrationTestDescriber
|
||||
}) => void
|
||||
|
||||
/**
|
||||
* Describes an integration test suite using wrappers over Mocha primitives.
|
||||
*
|
||||
* To record test data, set the RECORD environment variable to a truthy value.
|
||||
* When recording, the tests will be run in Puppeteer against a real backend,
|
||||
* and captured response fixtures will be saved to the `__fixtures__` directory.
|
||||
*
|
||||
* When running the tests, static CSS/JS assets will be served from the `ui/assets` directory.
|
||||
* Other requests (for instance to the GraphQL API) will be stubbed using response
|
||||
* stubs from the `__fixtures__` directory, through Puppeteer's request interception.
|
||||
*
|
||||
*/
|
||||
export function describeIntegration(description: string, testSuite: IntegrationTestSuite): void {
|
||||
const record = Boolean(process.env.RECORD)
|
||||
mochaDescribe(description, () => {
|
||||
let driver: Driver
|
||||
let sourcegraphBaseUrl: string
|
||||
const subscriptions = new Subscription()
|
||||
|
||||
const test = (prefixes: string[]): IntegrationTestDefiner => {
|
||||
const wrapTestBody = (title: string, run: TestBody) => async () => {
|
||||
await driver.newPage()
|
||||
await driver.page.setRequestInterception(true)
|
||||
const recordingsDirectory = path.join(FIXTURES_DIRECTORY, ...prefixes.map(snakeCase))
|
||||
if (record) {
|
||||
await mkdirp(recordingsDirectory)
|
||||
}
|
||||
const polly = new Polly(snakeCase(title), {
|
||||
adapters: ['puppeteer'],
|
||||
adapterOptions: {
|
||||
puppeteer: { page: driver.page, requestResourceTypes: ['xhr', 'fetch', 'document'] },
|
||||
},
|
||||
persister: 'fs',
|
||||
persisterOptions: {
|
||||
fs: {
|
||||
recordingsDir: recordingsDirectory,
|
||||
},
|
||||
},
|
||||
expiryStrategy: 'warn',
|
||||
recordIfMissing: record,
|
||||
matchRequestsBy: {
|
||||
method: true,
|
||||
body: true,
|
||||
order: true,
|
||||
// Origin header will change when running against a test instance
|
||||
headers: false,
|
||||
url: {
|
||||
pathname: true,
|
||||
query: true,
|
||||
hash: true,
|
||||
// Allow recording tests against https://sourcegraph.test
|
||||
// but running them against http:://localhost:8000
|
||||
protocol: false,
|
||||
port: false,
|
||||
hostname: false,
|
||||
username: false,
|
||||
password: false,
|
||||
},
|
||||
},
|
||||
mode: record ? 'record' : 'replay',
|
||||
logging: false,
|
||||
})
|
||||
const { server } = polly
|
||||
|
||||
const errors = new Subject<never>()
|
||||
|
||||
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<GraphQLRequestEvent<GraphQLOperationName>>()
|
||||
server.post(new URL('/.api/graphql', sourcegraphBaseUrl).href).intercept((request, response) => {
|
||||
// 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 "${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)
|
||||
return error
|
||||
}
|
||||
if (!graphQlOverrides || !keyExistsIn(operationName, graphQlOverrides)) {
|
||||
throw missingOverrideError()
|
||||
}
|
||||
const handler = graphQlOverrides[operationName]
|
||||
if (!handler) {
|
||||
throw missingOverrideError()
|
||||
}
|
||||
|
||||
try {
|
||||
const result = handler(variables as any)
|
||||
const graphQlResult: SuccessGraphQLResult<any> = { data: result, errors: undefined }
|
||||
response.json(graphQlResult)
|
||||
} catch (error) {
|
||||
if (!(error instanceof IntegrationTestGraphQlError)) {
|
||||
errors.error(error)
|
||||
throw error
|
||||
}
|
||||
|
||||
const graphQlError: ErrorGraphQLResult = { data: undefined, errors: error.errors }
|
||||
response.json(graphQlError)
|
||||
}
|
||||
})
|
||||
|
||||
// Serve all requests for index.html (everything that does not match the handlers above) the same index.html
|
||||
let jsContext = createJsContext({ sourcegraphBaseUrl })
|
||||
server.get(new URL('/*path', sourcegraphBaseUrl).href).intercept((request, response) => {
|
||||
response.type('text/html').send(html`
|
||||
<html>
|
||||
<head>
|
||||
<title>Sourcegraph Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script>
|
||||
window.context = ${JSON.stringify(jsContext)}
|
||||
</script>
|
||||
<script src="/.assets/scripts/app.bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
})
|
||||
|
||||
// Filter out 'server' header filled in by Caddy before persisting responses,
|
||||
// otherwise tests will hang when replayed from recordings.
|
||||
server
|
||||
.any()
|
||||
.on(
|
||||
'beforePersist',
|
||||
(request, recording: { response: { headers: { name: string; value: string }[] } }) => {
|
||||
recording.response.headers = recording.response.headers.filter(
|
||||
({ name }) => name !== 'server'
|
||||
)
|
||||
}
|
||||
)
|
||||
try {
|
||||
await Promise.race([
|
||||
errors.toPromise(),
|
||||
run({
|
||||
sourcegraphBaseUrl,
|
||||
driver,
|
||||
overrideGraphQL: overrides => {
|
||||
graphQlOverrides = overrides
|
||||
},
|
||||
overrideJsContext: override => {
|
||||
jsContext = override
|
||||
},
|
||||
waitForGraphQLRequest: async <O extends GraphQLOperationName>(
|
||||
triggerRequest: () => Promise<void> | void,
|
||||
operationName: O
|
||||
): Promise<Parameters<AllGraphQlOperations[O]>[0]> => {
|
||||
const requestPromise = graphQlRequests
|
||||
.pipe(
|
||||
first(
|
||||
(
|
||||
request: GraphQLRequestEvent<GraphQLOperationName>
|
||||
): request is GraphQLRequestEvent<O> =>
|
||||
request.operationName === operationName
|
||||
),
|
||||
timeoutWith(
|
||||
4000,
|
||||
throwError(
|
||||
new Error(`Timeout waiting for GraphQL request "${operationName}"`)
|
||||
)
|
||||
)
|
||||
)
|
||||
.toPromise()
|
||||
await triggerRequest()
|
||||
const { variables } = await requestPromise
|
||||
return variables
|
||||
},
|
||||
}),
|
||||
])
|
||||
} finally {
|
||||
await polly.stop()
|
||||
await recordCoverage(driver.browser)
|
||||
await driver.page.close()
|
||||
}
|
||||
}
|
||||
return Object.assign(
|
||||
(title: string, run: TestBody) => {
|
||||
mochaTest(title, wrapTestBody(title, run))
|
||||
},
|
||||
{
|
||||
only: (title: string, run: TestBody) => {
|
||||
mochaTest.only(title, wrapTestBody(title, run))
|
||||
},
|
||||
skip: (title: string) => {
|
||||
mochaTest.skip(title)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
const before: IntegrationTestBeforeGeneration = setupLogic => {
|
||||
if (record) {
|
||||
mochaBefore(() => setupLogic({ driver, sourcegraphBaseUrl }))
|
||||
}
|
||||
}
|
||||
const describe = (prefixes: string[]): IntegrationTestDescriber => (title, suite) => {
|
||||
mochaDescribe(title, () => {
|
||||
suite({
|
||||
describe: describe([...prefixes, title]),
|
||||
before,
|
||||
it: test([...prefixes, title]),
|
||||
test: test([...prefixes, title]),
|
||||
})
|
||||
})
|
||||
}
|
||||
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
|
||||
}
|
||||
testSuite({ initGeneration, test: test([description]), describe: describe([description]) })
|
||||
mochaBefore(async () => {
|
||||
if (!initGenerationCallback) {
|
||||
throw new Error('initGeneration() was never called')
|
||||
}
|
||||
if (record) {
|
||||
const setupResult = await initGenerationCallback()
|
||||
driver = setupResult.driver
|
||||
sourcegraphBaseUrl = setupResult.sourcegraphBaseUrl
|
||||
if (setupResult.subscriptions) {
|
||||
subscriptions.add(setupResult.subscriptions)
|
||||
}
|
||||
} else {
|
||||
sourcegraphBaseUrl = 'http://localhost:8000'
|
||||
driver = await createDriverForTest({
|
||||
sourcegraphBaseUrl,
|
||||
logBrowserConsole: false,
|
||||
})
|
||||
// Serve static assets from `ui/assets`
|
||||
const app = express()
|
||||
app.use('/.assets', express.static(ASSETS_DIRECTORY))
|
||||
const server = app.listen(8000)
|
||||
subscriptions.add(() => server.close())
|
||||
}
|
||||
})
|
||||
after(async () => {
|
||||
await driver?.close()
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
subscriptions?.unsubscribe()
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -1,9 +1,11 @@
|
||||
import assert from 'assert'
|
||||
import expect from 'expect'
|
||||
import { describeIntegration } from './helpers'
|
||||
import { commonGraphQlResults } from './graphQlResults'
|
||||
import { commonWebGraphQlResults } from './graphQlResults'
|
||||
import { ILanguage, IRepository } from '../../../shared/src/graphql/schema'
|
||||
import { SearchResult } from '../graphql-operations'
|
||||
import { Driver, createDriverForTest } from '../../../shared/src/testing/driver'
|
||||
import { WebIntegrationTestContext, createWebIntegrationTestContext } from './context'
|
||||
import { test } from 'mocha'
|
||||
|
||||
const searchResults = (): SearchResult => ({
|
||||
search: {
|
||||
@ -62,19 +64,34 @@ const searchResults = (): SearchResult => ({
|
||||
},
|
||||
})
|
||||
|
||||
describeIntegration('Search', ({ describe }) => {
|
||||
describe('Interactive search mode', ({ test }) => {
|
||||
test('Search mode component appears', async ({ sourcegraphBaseUrl, driver }) => {
|
||||
await driver.page.goto(sourcegraphBaseUrl + '/search')
|
||||
describe('Search', () => {
|
||||
let driver: Driver
|
||||
before(async () => {
|
||||
driver = await createDriverForTest()
|
||||
})
|
||||
after(() => driver?.close())
|
||||
let testContext: WebIntegrationTestContext
|
||||
beforeEach(async function () {
|
||||
testContext = await createWebIntegrationTestContext({
|
||||
driver,
|
||||
currentTest: this.currentTest!,
|
||||
directory: __dirname,
|
||||
})
|
||||
})
|
||||
afterEach(() => testContext?.dispose())
|
||||
|
||||
describe('Interactive search mode', () => {
|
||||
test('Search mode component appears', async () => {
|
||||
await driver.page.goto(driver.sourcegraphBaseUrl + '/search')
|
||||
await driver.page.waitForSelector('.e2e-search-mode-toggle')
|
||||
expect(await driver.page.evaluate(() => document.querySelectorAll('.e2e-search-mode-toggle').length)).toBe(
|
||||
1
|
||||
)
|
||||
})
|
||||
|
||||
test('Filter buttons', async ({ sourcegraphBaseUrl, driver, overrideGraphQL }) => {
|
||||
overrideGraphQL({
|
||||
...commonGraphQlResults,
|
||||
test('Filter buttons', async () => {
|
||||
testContext.overrideGraphQL({
|
||||
...commonWebGraphQlResults,
|
||||
SearchSuggestions: () => ({
|
||||
search: {
|
||||
suggestions: [
|
||||
@ -94,7 +111,7 @@ describeIntegration('Search', ({ describe }) => {
|
||||
],
|
||||
}),
|
||||
})
|
||||
await driver.page.goto(sourcegraphBaseUrl + '/search')
|
||||
await driver.page.goto(driver.sourcegraphBaseUrl + '/search')
|
||||
await driver.page.waitForSelector('.e2e-search-mode-toggle', { visible: true })
|
||||
await driver.page.click('.e2e-search-mode-toggle')
|
||||
await driver.page.click('.e2e-search-mode-toggle__interactive-mode')
|
||||
@ -138,13 +155,13 @@ describeIntegration('Search', ({ describe }) => {
|
||||
await driver.assertWindowLocation('/search?q=repo:%22gorilla/mux%22+file:%22README%22&patternType=literal')
|
||||
|
||||
// Delete filter
|
||||
await driver.page.goto(sourcegraphBaseUrl + '/search?q=repo:gorilla/mux&patternType=literal')
|
||||
await driver.page.goto(driver.sourcegraphBaseUrl + '/search?q=repo:gorilla/mux&patternType=literal')
|
||||
await driver.page.waitForSelector('.e2e-filter-input__delete-button', { visible: true })
|
||||
await driver.page.click('.e2e-filter-input__delete-button')
|
||||
await driver.assertWindowLocation('/search?q=&patternType=literal')
|
||||
|
||||
// Test suggestions
|
||||
await driver.page.goto(sourcegraphBaseUrl + '/search')
|
||||
await driver.page.goto(driver.sourcegraphBaseUrl + '/search')
|
||||
await driver.page.waitForSelector('.e2e-add-filter-button-repo', { visible: true })
|
||||
await driver.page.click('.e2e-add-filter-button-repo')
|
||||
await driver.page.waitForSelector('.filter-input', { visible: true })
|
||||
@ -179,13 +196,9 @@ describeIntegration('Search', ({ describe }) => {
|
||||
)
|
||||
})
|
||||
|
||||
test('Updates query when searching from directory page', async ({
|
||||
sourcegraphBaseUrl,
|
||||
driver,
|
||||
overrideGraphQL,
|
||||
}) => {
|
||||
overrideGraphQL({
|
||||
...commonGraphQlResults,
|
||||
test('Updates query when searching from directory page', async () => {
|
||||
testContext.overrideGraphQL({
|
||||
...commonWebGraphQlResults,
|
||||
RepositoryRedirect: () => ({
|
||||
repositoryRedirect: {
|
||||
__typename: 'Repository',
|
||||
@ -211,7 +224,7 @@ describeIntegration('Search', ({ describe }) => {
|
||||
},
|
||||
}),
|
||||
})
|
||||
await driver.page.goto(sourcegraphBaseUrl + '/github.com/sourcegraph/jsonrpc2')
|
||||
await driver.page.goto(driver.sourcegraphBaseUrl + '/github.com/sourcegraph/jsonrpc2')
|
||||
await driver.page.waitForSelector('.filter-input')
|
||||
const filterInputValue = () =>
|
||||
driver.page.evaluate(() => {
|
||||
@ -221,16 +234,12 @@ describeIntegration('Search', ({ describe }) => {
|
||||
assert.strictEqual(await filterInputValue(), 'repo:^github\\.com/sourcegraph/jsonrpc2$')
|
||||
})
|
||||
|
||||
test('Filter dropdown and finite-option filter inputs', async ({
|
||||
sourcegraphBaseUrl,
|
||||
driver,
|
||||
overrideGraphQL,
|
||||
}) => {
|
||||
overrideGraphQL({
|
||||
...commonGraphQlResults,
|
||||
test('Filter dropdown and finite-option filter inputs', async () => {
|
||||
testContext.overrideGraphQL({
|
||||
...commonWebGraphQlResults,
|
||||
Search: searchResults,
|
||||
})
|
||||
await driver.page.goto(sourcegraphBaseUrl + '/search')
|
||||
await driver.page.goto(driver.sourcegraphBaseUrl + '/search')
|
||||
await driver.page.waitForSelector('.e2e-query-input', { visible: true })
|
||||
await driver.page.waitForSelector('.e2e-filter-dropdown')
|
||||
await driver.page.type('.e2e-query-input', 'test')
|
||||
@ -261,9 +270,9 @@ describeIntegration('Search', ({ describe }) => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Case sensitivity toggle', ({ test }) => {
|
||||
test('Clicking toggle turns on case sensitivity', async ({ sourcegraphBaseUrl, driver }) => {
|
||||
await driver.page.goto(sourcegraphBaseUrl + '/search')
|
||||
describe('Case sensitivity toggle', () => {
|
||||
test('Clicking toggle turns on case sensitivity', async () => {
|
||||
await driver.page.goto(driver.sourcegraphBaseUrl + '/search')
|
||||
await driver.page.waitForSelector('.e2e-query-input', { visible: true })
|
||||
await driver.page.waitForSelector('.e2e-case-sensitivity-toggle')
|
||||
await driver.page.type('.e2e-query-input', 'test')
|
||||
@ -271,16 +280,12 @@ describeIntegration('Search', ({ describe }) => {
|
||||
await driver.assertWindowLocation('/search?q=test&patternType=literal&case=yes')
|
||||
})
|
||||
|
||||
test('Clicking toggle turns off case sensitivity and removes case= URL parameter', async ({
|
||||
sourcegraphBaseUrl,
|
||||
driver,
|
||||
overrideGraphQL,
|
||||
}) => {
|
||||
overrideGraphQL({
|
||||
...commonGraphQlResults,
|
||||
test('Clicking toggle turns off case sensitivity and removes case= URL parameter', async () => {
|
||||
testContext.overrideGraphQL({
|
||||
...commonWebGraphQlResults,
|
||||
Search: searchResults,
|
||||
})
|
||||
await driver.page.goto(sourcegraphBaseUrl + '/search?q=test&patternType=literal&case=yes')
|
||||
await driver.page.goto(driver.sourcegraphBaseUrl + '/search?q=test&patternType=literal&case=yes')
|
||||
await driver.page.waitForSelector('.e2e-query-input', { visible: true })
|
||||
await driver.page.waitForSelector('.e2e-case-sensitivity-toggle')
|
||||
await driver.page.click('.e2e-case-sensitivity-toggle')
|
||||
@ -288,13 +293,13 @@ describeIntegration('Search', ({ describe }) => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Structural search toggle', ({ test }) => {
|
||||
test('Clicking toggle turns on structural search', async ({ sourcegraphBaseUrl, driver, overrideGraphQL }) => {
|
||||
overrideGraphQL({
|
||||
...commonGraphQlResults,
|
||||
describe('Structural search toggle', () => {
|
||||
test('Clicking toggle turns on structural search', async () => {
|
||||
testContext.overrideGraphQL({
|
||||
...commonWebGraphQlResults,
|
||||
Search: searchResults,
|
||||
})
|
||||
await driver.page.goto(sourcegraphBaseUrl + '/search')
|
||||
await driver.page.goto(driver.sourcegraphBaseUrl + '/search')
|
||||
await driver.page.waitForSelector('.e2e-query-input', { visible: true })
|
||||
await driver.page.waitForSelector('.e2e-structural-search-toggle')
|
||||
await driver.page.type('.e2e-query-input', 'test')
|
||||
@ -302,32 +307,24 @@ describeIntegration('Search', ({ describe }) => {
|
||||
await driver.assertWindowLocation('/search?q=test&patternType=structural')
|
||||
})
|
||||
|
||||
test('Clicking toggle turns on structural search and removes existing patternType parameter', async ({
|
||||
sourcegraphBaseUrl,
|
||||
driver,
|
||||
overrideGraphQL,
|
||||
}) => {
|
||||
overrideGraphQL({
|
||||
...commonGraphQlResults,
|
||||
test('Clicking toggle turns on structural search and removes existing patternType parameter', async () => {
|
||||
testContext.overrideGraphQL({
|
||||
...commonWebGraphQlResults,
|
||||
Search: searchResults,
|
||||
})
|
||||
await driver.page.goto(sourcegraphBaseUrl + '/search?q=test&patternType=regexp')
|
||||
await driver.page.goto(driver.sourcegraphBaseUrl + '/search?q=test&patternType=regexp')
|
||||
await driver.page.waitForSelector('.e2e-query-input', { visible: true })
|
||||
await driver.page.waitForSelector('.e2e-structural-search-toggle')
|
||||
await driver.page.click('.e2e-structural-search-toggle')
|
||||
await driver.assertWindowLocation('/search?q=test&patternType=structural')
|
||||
})
|
||||
|
||||
test('Clicking toggle turns off structural saerch and reverts to default pattern type', async ({
|
||||
sourcegraphBaseUrl,
|
||||
driver,
|
||||
overrideGraphQL,
|
||||
}) => {
|
||||
overrideGraphQL({
|
||||
...commonGraphQlResults,
|
||||
test('Clicking toggle turns off structural saerch and reverts to default pattern type', async () => {
|
||||
testContext.overrideGraphQL({
|
||||
...commonWebGraphQlResults,
|
||||
Search: searchResults,
|
||||
})
|
||||
await driver.page.goto(sourcegraphBaseUrl + '/search?q=test&patternType=structural')
|
||||
await driver.page.goto(driver.sourcegraphBaseUrl + '/search?q=test&patternType=structural')
|
||||
await driver.page.waitForSelector('.e2e-query-input', { visible: true })
|
||||
await driver.page.waitForSelector('.e2e-structural-search-toggle')
|
||||
await driver.page.click('.e2e-structural-search-toggle')
|
||||
|
||||
@ -1,13 +1,30 @@
|
||||
import assert from 'assert'
|
||||
import { describeIntegration } from './helpers'
|
||||
import { retry } from '../../../shared/src/testing/utils'
|
||||
import { commonGraphQlResults, testUserID, settingsID } from './graphQlResults'
|
||||
import { createDriverForTest, Driver } from '../../../shared/src/testing/driver'
|
||||
import { commonWebGraphQlResults } from './graphQlResults'
|
||||
import { createWebIntegrationTestContext, WebIntegrationTestContext } from './context'
|
||||
import { settingsID, testUserID } from '../../../shared/src/testing/integration/graphQlResults'
|
||||
|
||||
describeIntegration('Settings', ({ describe }) => {
|
||||
describe('User settings page', ({ it }) => {
|
||||
it('updates user settings', async ({ driver, sourcegraphBaseUrl, overrideGraphQL, waitForGraphQLRequest }) => {
|
||||
overrideGraphQL({
|
||||
...commonGraphQlResults,
|
||||
describe('Settings', () => {
|
||||
let driver: Driver
|
||||
before(async () => {
|
||||
driver = await createDriverForTest()
|
||||
})
|
||||
after(() => driver?.close())
|
||||
let testContext: WebIntegrationTestContext
|
||||
beforeEach(async function () {
|
||||
testContext = await createWebIntegrationTestContext({
|
||||
driver,
|
||||
currentTest: this.currentTest!,
|
||||
directory: __dirname,
|
||||
})
|
||||
})
|
||||
afterEach(() => testContext?.dispose())
|
||||
|
||||
describe('User settings page', () => {
|
||||
it('updates user settings', async () => {
|
||||
testContext.overrideGraphQL({
|
||||
...commonWebGraphQlResults,
|
||||
SettingsCascade: () => ({
|
||||
settingsSubject: {
|
||||
settingsCascade: {
|
||||
@ -61,7 +78,7 @@ describeIntegration('Settings', ({ describe }) => {
|
||||
)
|
||||
}
|
||||
|
||||
await driver.page.goto(sourcegraphBaseUrl + '/users/test/settings')
|
||||
await driver.page.goto(driver.sourcegraphBaseUrl + '/users/test/settings')
|
||||
|
||||
await driver.page.waitForSelector('.e2e-settings-file .monaco-editor')
|
||||
await driver.page.waitForSelector('.e2e-save-toolbar-save')
|
||||
@ -96,7 +113,7 @@ describeIntegration('Settings', ({ describe }) => {
|
||||
)
|
||||
|
||||
// Assert mutation is done when save button is clicked
|
||||
const overrideSettingsVariables = await waitForGraphQLRequest(async () => {
|
||||
const overrideSettingsVariables = await testContext.waitForGraphQLRequest(async () => {
|
||||
await driver.findElementWithText('Save changes', { action: 'click' })
|
||||
}, 'OverwriteSettings')
|
||||
|
||||
|
||||
13
yarn.lock
13
yarn.lock
@ -2284,7 +2284,8 @@
|
||||
integrity sha512-KWxkyphmlwam8kfYPSmoitKQRMGQCsr1ZRmNZgijT7ABKaVyk/+I5ezt2J213tM04Hi0vyg4L7iH1VCkNvm2Jw==
|
||||
|
||||
"@sourcegraph/extension-api-types@link:packages/@sourcegraph/extension-api-types":
|
||||
version "2.1.0"
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@sourcegraph/prettierrc@^3.0.3":
|
||||
version "3.0.3"
|
||||
@ -3508,6 +3509,11 @@
|
||||
resolved "https://registry.npmjs.org/@types/marked/-/marked-1.1.0.tgz#53509b5f127e0c05c19176fcf1d743a41e00ff19"
|
||||
integrity sha512-j8XXj6/l9kFvCwMyVqozznqpd/nk80krrW+QiIJN60Uu9gX5Pvn4/qPJ2YngQrR3QREPwmrE1f9/EWKVTFzoEw==
|
||||
|
||||
"@types/mime-types@2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.0.tgz#9ca52cda363f699c69466c2a6ccdaad913ea7a73"
|
||||
integrity sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM=
|
||||
|
||||
"@types/mime@*":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b"
|
||||
@ -14710,7 +14716,7 @@ mime-db@1.44.0, "mime-db@>= 1.40.0 < 2":
|
||||
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
|
||||
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
|
||||
|
||||
mime-types@^2.1.12, mime-types@^2.1.14, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24:
|
||||
mime-types@^2.1.12, mime-types@^2.1.14, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24:
|
||||
version "2.1.27"
|
||||
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
|
||||
integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
|
||||
@ -19940,7 +19946,8 @@ sourcegraph@^24.0.0:
|
||||
integrity sha512-FEmuGVRcSqMtE5DItZcDa1ylm7FykcswIN6M6xa0vFbyLv6INYxjGYOGq/WlH4/AUQC0abmaysPXlyJRkGgMyg==
|
||||
|
||||
"sourcegraph@link:packages/sourcegraph-extension-api":
|
||||
version "24.6.0"
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
space-separated-tokens@^1.0.0:
|
||||
version "1.1.2"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user