JetBrains: Add storybook for Chromatic tests (#38639)

* Add storybook for JetBrains, and hook it up
* Add custom webpack settings for JetBrains
* Bug fix: add missing commit from path matches
* Fix initial search in standalone version
* Order scripts alphabetically
* Extract callJava
This commit is contained in:
David Veszelovszki 2022-07-15 12:45:58 +02:00 committed by GitHub
parent 6c8e51cdde
commit 167b585f58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 424 additions and 184 deletions

View File

@ -13,13 +13,14 @@
"directory": "client/jetbrains"
},
"scripts": {
"task:gulp": "cross-env NODE_OPTIONS=\"--max_old_space_size=8192\" gulp",
"build": "yarn task:gulp esbuild ",
"watch": "WATCH=true yarn task:gulp esbuild",
"standalone": "ts-node standalone/src/server.ts",
"build": "yarn task:gulp esbuild",
"lint": "yarn run lint:js && yarn run lint:css",
"lint:js": "eslint --cache 'webview/**/*.[jt]s?(x)'",
"lint:css": "stylelint 'webview/**/*.scss'",
"typecheck": "tsc -b"
"standalone": "ts-node standalone/src/server.ts",
"storybook": "STORIES_GLOB=client/jetbrains/webview/src/**/*.story.tsx yarn workspace @sourcegraph/storybook run start",
"task:gulp": "cross-env NODE_OPTIONS=\"--max_old_space_size=8192\" gulp",
"typecheck": "tsc -b",
"watch": "WATCH=true yarn task:gulp esbuild"
}
}

View File

@ -0,0 +1,166 @@
import { decode } from 'js-base64'
import { SearchPatternType } from '@sourcegraph/search'
import type { PreviewRequest, Request } from '../search/js-to-java-bridge'
import type { Search, Theme } from '../search/types'
import { dark } from './theme-snapshots/dark'
import { light } from './theme-snapshots/light'
const instanceURL = 'https://sourcegraph.com/'
let isDarkTheme = false
const savedSearchFromLocalStorage = localStorage.getItem('savedSearch')
let savedSearch: Search = savedSearchFromLocalStorage
? (JSON.parse(savedSearchFromLocalStorage) as Search)
: {
query: 'r:github.com/sourcegraph/sourcegraph jetbrains',
caseSensitive: false,
patternType: SearchPatternType.literal,
selectedSearchContextSpec: 'global',
}
const codeDetailsNode = document.querySelector('#code-details') as HTMLPreElement
let previewContent: PreviewRequest['arguments'] | null = null
export function callJava(request: Request): Promise<object> {
return new Promise((resolve, reject) => {
const requestAsString = JSON.stringify(request)
const onSuccessCallback = (responseAsString: string): void => {
resolve(JSON.parse(responseAsString))
}
const onFailureCallback = (errorCode: number, errorMessage: string): void => {
reject(new Error(`${errorCode} - ${errorMessage}`))
}
console.log(`Got this request: ${requestAsString}`)
handleRequest(request, onSuccessCallback, onFailureCallback)
})
}
function handleRequest(
request: Request,
onSuccessCallback: (responseAsString: string) => void,
onFailureCallback: (errorCode: number, errorMessage: string) => void
): void {
const action = request.action
switch (action) {
case 'getConfig': {
onSuccessCallback(
JSON.stringify({
instanceURL,
isGlobbingEnabled: true,
accessToken: null,
anonymousUserId: 'test',
pluginVersion: '1.2.3',
})
)
break
}
case 'getTheme': {
const theme: Theme = isDarkTheme ? dark : light
onSuccessCallback(JSON.stringify(theme))
break
}
case 'previewLoading': {
codeDetailsNode.innerHTML = 'Loading...'
onSuccessCallback('null')
break
}
case 'preview': {
previewContent = request.arguments
const start =
previewContent.absoluteOffsetAndLengths && previewContent.absoluteOffsetAndLengths.length > 0
? previewContent.absoluteOffsetAndLengths[0][0]
: 0
const length =
previewContent.absoluteOffsetAndLengths && previewContent.absoluteOffsetAndLengths.length > 0
? previewContent.absoluteOffsetAndLengths[0][1]
: 0
let htmlContent: string
if (previewContent.content === null) {
htmlContent = 'No preview available'
} else {
const decodedContent = decode(previewContent.content || '')
htmlContent = escapeHTML(decodedContent.slice(0, start))
htmlContent += `<span id="code-details-highlight">${escapeHTML(
decodedContent.slice(start, start + length)
)}</span>`
htmlContent += escapeHTML(decodedContent.slice(start + length))
}
codeDetailsNode.innerHTML = htmlContent
document.querySelector('#code-details-highlight')?.scrollIntoView({ block: 'center', inline: 'center' })
onSuccessCallback('null')
break
}
case 'clearPreview': {
codeDetailsNode.textContent = ''
onSuccessCallback('null')
break
}
case 'open': {
previewContent = request.arguments
if (previewContent.fileName) {
alert(`Now the IDE would open ${previewContent.path} in the editor...`)
} else {
window.open(instanceURL + (previewContent.relativeUrl || ''), '_blank')
}
onSuccessCallback('null')
break
}
case 'saveLastSearch': {
savedSearch = request.arguments
localStorage.setItem('savedSearch', JSON.stringify(savedSearch))
onSuccessCallback('null')
break
}
case 'loadLastSearch': {
onSuccessCallback(JSON.stringify(savedSearch))
break
}
case 'indicateFinishedLoading': {
onSuccessCallback('null')
break
}
case 'windowClose': {
console.log('Closing window')
onSuccessCallback('null')
break
}
default: {
// noinspection UnnecessaryLocalVariableJS
const exhaustiveCheck: never = action
onFailureCallback(2, `Unknown action: ${exhaustiveCheck as string}`)
}
}
}
export function setDarkMode(value: boolean): void {
isDarkTheme = value
}
function escapeHTML(unsafe: string): string {
return unsafe.replace(
// eslint-disable-next-line no-control-regex
/[\u0000-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u00FF]/g,
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
char => '&#' + ('000' + char.charCodeAt(0)).slice(-4) + ';'
)
}

View File

@ -1,157 +1,8 @@
import { decode } from 'js-base64'
import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations'
import type { PreviewRequest, Request } from '../search/js-to-java-bridge'
import type { Search, Theme } from '../search/types'
import { callJava, setDarkMode } from './call-java-mock'
import { renderColorDebugger } from './renderColorDebugger'
import { dark } from './theme-snapshots/dark'
import { light } from './theme-snapshots/light'
const instanceURL = 'https://sourcegraph.com/'
let isDarkTheme = false
const codeDetailsNode = document.querySelector('#code-details') as HTMLPreElement
const iframeNode = document.querySelector('#webview') as HTMLIFrameElement
const savedSearchFromLocalStorage = localStorage.getItem('savedSearch')
let savedSearch: Search = savedSearchFromLocalStorage
? (JSON.parse(savedSearchFromLocalStorage) as Search)
: {
query: 'r:github.com/sourcegraph/sourcegraph jetbrains',
caseSensitive: false,
patternType: SearchPatternType.literal,
selectedSearchContextSpec: 'global',
}
let previewContent: PreviewRequest['arguments'] | null = null
function callJava(request: Request): Promise<object> {
return new Promise((resolve, reject) => {
const requestAsString = JSON.stringify(request)
const onSuccessCallback = (responseAsString: string): void => {
resolve(JSON.parse(responseAsString))
}
const onFailureCallback = (errorCode: number, errorMessage: string): void => {
reject(new Error(`${errorCode} - ${errorMessage}`))
}
console.log(`Got this request: ${requestAsString}`)
handleRequest(request, onSuccessCallback, onFailureCallback)
})
}
function handleRequest(
request: Request,
onSuccessCallback: (responseAsString: string) => void,
onFailureCallback: (errorCode: number, errorMessage: string) => void
): void {
const action = request.action
switch (action) {
case 'getConfig': {
onSuccessCallback(
JSON.stringify({
instanceURL,
isGlobbingEnabled: true,
accessToken: null,
anonymousUserId: 'test',
pluginVersion: '1.2.3',
})
)
break
}
case 'getTheme': {
const theme: Theme = isDarkTheme ? dark : light
onSuccessCallback(JSON.stringify(theme))
break
}
case 'previewLoading': {
codeDetailsNode.innerHTML = 'Loading...'
onSuccessCallback('null')
break
}
case 'preview': {
previewContent = request.arguments
const start =
previewContent.absoluteOffsetAndLengths && previewContent.absoluteOffsetAndLengths.length > 0
? previewContent.absoluteOffsetAndLengths[0][0]
: 0
const length =
previewContent.absoluteOffsetAndLengths && previewContent.absoluteOffsetAndLengths.length > 0
? previewContent.absoluteOffsetAndLengths[0][1]
: 0
let htmlContent: string
if (previewContent.content === null) {
htmlContent = 'No preview available'
} else {
const decodedContent = decode(previewContent.content || '')
htmlContent = escapeHTML(decodedContent.slice(0, start))
htmlContent += `<span id="code-details-highlight">${escapeHTML(
decodedContent.slice(start, start + length)
)}</span>`
htmlContent += escapeHTML(decodedContent.slice(start + length))
}
codeDetailsNode.innerHTML = htmlContent
document.querySelector('#code-details-highlight')?.scrollIntoView({ block: 'center', inline: 'center' })
onSuccessCallback('null')
break
}
case 'clearPreview': {
codeDetailsNode.textContent = ''
onSuccessCallback('null')
break
}
case 'open': {
previewContent = request.arguments
if (previewContent.fileName) {
alert(`Now the IDE would open ${previewContent.path} in the editor...`)
} else {
window.open(instanceURL + (previewContent.relativeUrl || ''), '_blank')
}
onSuccessCallback('null')
break
}
case 'saveLastSearch': {
savedSearch = request.arguments
localStorage.setItem('savedSearch', JSON.stringify(savedSearch))
onSuccessCallback('null')
break
}
case 'loadLastSearch': {
onSuccessCallback(JSON.stringify(savedSearch))
break
}
case 'indicateFinishedLoading': {
onSuccessCallback('null')
break
}
case 'windowClose': {
console.log('Closing window')
onSuccessCallback('null')
break
}
default: {
const exhaustiveCheck: never = action
onFailureCallback(2, `Unknown action: ${exhaustiveCheck as string}`)
}
}
}
// Initialize app for standalone server
iframeNode.addEventListener('load', () => {
const iframeWindow = iframeNode.contentWindow
@ -166,21 +17,12 @@ iframeNode.addEventListener('load', () => {
// Detect dark or light mode preference
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
isDarkTheme = true
setDarkMode(true)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
document.body.parentElement!.className = 'dark'
}
// Render the theme color debuggerwhen the URL contains `?color-debug`
// Render the theme color debugger when the URL contains `?color-debug`
if (location.href.includes('color-debug')) {
renderColorDebugger()
}
function escapeHTML(unsafe: string): string {
return unsafe.replace(
// eslint-disable-next-line no-control-regex
/[\u0000-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u00FF]/g,
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
char => '&#' + ('000' + char.charCodeAt(0)).slice(-4) + ';'
)
}

View File

@ -10,8 +10,9 @@ import {
QueryState,
SearchPatternType,
} from '@sourcegraph/search'
import { AuthenticatedUser } from '@sourcegraph/shared/src/auth'
import { PlatformContext } from '@sourcegraph/shared/src/platform/context'
import type { TelemetryService } from '@sourcegraph/shared/out/src/telemetry/telemetryService'
import type { AuthenticatedUser } from '@sourcegraph/shared/src/auth'
import type { PlatformContext } from '@sourcegraph/shared/src/platform/context'
import {
aggregateStreamingSearch,
LATEST_VERSION,
@ -24,7 +25,6 @@ import { EMPTY_SETTINGS_CASCADE, SettingsCascadeOrError } from '@sourcegraph/sha
import { useObservable, WildcardThemeContext } from '@sourcegraph/wildcard'
import { initializeSourcegraphSettings } from '../sourcegraphSettings'
import { EventLogger } from '../telemetry/EventLogger'
import { GlobalKeyboardListeners } from './GlobalKeyboardListeners'
import { JetBrainsSearchBox } from './input/JetBrainsSearchBox'
@ -47,7 +47,7 @@ interface Props {
onOpen: (match: SearchMatch, lineOrSymbolMatchIndex?: number) => Promise<void>
initialSearch: Search | null
authenticatedUser: AuthenticatedUser | null
telemetryService: EventLogger
telemetryService: TelemetryService
}
function fetchStreamSuggestionsWithStaticUrl(query: string): Observable<SearchMatch[]> {

View File

@ -95,11 +95,11 @@ export function applyConfig(config: PluginConfig): void {
polyfillEventSource(accessToken ? { Authorization: `token ${accessToken}` } : {})
}
export function applyTheme(theme: Theme): void {
export function applyTheme(theme: Theme, rootElement: Element = document.documentElement): void {
// Dark/light theme
document.documentElement.classList.add('theme')
document.documentElement.classList.remove(theme.isDarkTheme ? 'theme-light' : 'theme-dark')
document.documentElement.classList.add(theme.isDarkTheme ? 'theme-dark' : 'theme-light')
rootElement.classList.add('theme')
rootElement.classList.remove(theme.isDarkTheme ? 'theme-light' : 'theme-dark')
rootElement.classList.add(theme.isDarkTheme ? 'theme-dark' : 'theme-light')
isDarkTheme = theme.isDarkTheme
// Find the name of properties here: https://plugins.jetbrains.com/docs/intellij/themes-metadata.html#key-naming-scheme

View File

@ -0,0 +1,90 @@
import { useEffect, useRef } from 'react'
import { DecoratorFn, Meta, Story } from '@storybook/react'
import { EMPTY, NEVER } from 'rxjs'
import { useDarkMode } from 'storybook-dark-mode'
import { SearchPatternType } from '@sourcegraph/search'
import { EMPTY_SETTINGS_CASCADE } from '@sourcegraph/shared/src/settings/settings'
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { usePrependStyles } from '@sourcegraph/storybook'
import { applyTheme } from '..'
import { dark } from '../../bridge-mock/theme-snapshots/dark'
import { light } from '../../bridge-mock/theme-snapshots/light'
import { JetBrainsSearchBox } from './JetBrainsSearchBox'
import globalStyles from '../../index.scss'
const decorator: DecoratorFn = story => <div className="p-3 container">{story()}</div>
const config: Meta = {
title: 'jetbrains/JetBrainsSearchBox',
decorators: [decorator],
}
export default config
export const JetBrainsSearchBoxStory: Story = () => {
const rootElementRef = useRef<HTMLDivElement>(null)
const isDarkTheme = useDarkMode()
usePrependStyles('branded-story-styles', globalStyles)
useEffect(() => {
if (rootElementRef.current === null) {
return
}
applyTheme(isDarkTheme ? dark : light, rootElementRef.current)
}, [rootElementRef, isDarkTheme])
return (
<div ref={rootElementRef}>
<div className="d-flex justify-content-center">
<div className="mx-6">
<JetBrainsSearchBox
caseSensitive={true}
setCaseSensitivity={() => {}}
patternType={SearchPatternType.regexp}
setPatternType={() => {}}
isSourcegraphDotCom={false}
structuralSearchDisabled={false}
queryState={{ query: 'type:file test AND test repo:contains.file(CHANGELOG)' }}
onChange={() => {}}
onSubmit={() => {}}
authenticatedUser={null}
searchContextsEnabled={true}
showSearchContext={true}
showSearchContextManagement={false}
defaultSearchContextSpec="global"
setSelectedSearchContextSpec={() => {}}
selectedSearchContextSpec={undefined}
fetchSearchContexts={() => {
throw new Error('fetchSearchContexts')
}}
fetchAutoDefinedSearchContexts={() => NEVER}
getUserSearchContextNamespaces={() => []}
fetchStreamSuggestions={() => NEVER}
settingsCascade={EMPTY_SETTINGS_CASCADE}
globbing={false}
isLightTheme={!isDarkTheme}
telemetryService={NOOP_TELEMETRY_SERVICE}
platformContext={{ requestGraphQL: () => EMPTY }}
className=""
containerClassName=""
autoFocus={true}
editorComponent="monaco"
hideHelpButton={true}
/>
</div>
</div>
</div>
)
}
JetBrainsSearchBoxStory.parameters = {
chromatic: {
disableSnapshot: false,
},
}

View File

@ -301,6 +301,7 @@ async function createPreviewContentForPathMatch(match: PathMatch): Promise<Previ
resultType: match.type,
fileName,
repoUrl: match.repository,
commit: match.commit,
path: match.path,
content: encodeContent(content),
}

View File

@ -0,0 +1,122 @@
import { useEffect, useRef } from 'react'
import { DecoratorFn, Meta, Story } from '@storybook/react'
import { useDarkMode } from 'storybook-dark-mode'
import { SymbolKind } from '@sourcegraph/search'
import { SearchMatch } from '@sourcegraph/shared/out/src/search/stream'
import { usePrependStyles } from '@sourcegraph/storybook'
import { applyTheme } from '..'
import { dark } from '../../bridge-mock/theme-snapshots/dark'
import { light } from '../../bridge-mock/theme-snapshots/light'
import { SearchResultList } from './SearchResultList'
import globalStyles from '../../index.scss'
const decorator: DecoratorFn = story => <div className="p-3 container">{story()}</div>
const config: Meta = {
title: 'jetbrains/SearchResultList',
decorators: [decorator],
}
export default config
export const JetBrainsSearchResultListStory: Story = () => {
const rootElementRef = useRef<HTMLDivElement>(null)
const isDarkTheme = useDarkMode()
const matches: SearchMatch[] = [
{
type: 'content',
path: '/CHANGELOG.md',
repository: 'test-repository',
repoStars: 1,
branches: ['a', 'b'],
commit: 'hunk12ef',
lineMatches: [
{ line: 'Test line 1', lineNumber: 0, offsetAndLengths: [] },
{ line: 'Test line 5', lineNumber: 4, offsetAndLengths: [] },
],
},
{
type: 'repo',
repository: 'test-repository',
repoStars: 2,
description: 'Repo description',
fork: true,
archived: true,
private: true,
branches: ['a', 'b'],
},
{
type: 'commit',
url: 'https://github.com/sourcegraph/sourcegraph',
repository: 'test-repository',
oid: 'hunk12ef',
message: 'Commit message',
authorName: 'Test User',
authorDate: '2022-01-01T00:00:00Z',
repoStars: 3,
content: '',
// Array of [line, character, length] triplets
ranges: [],
},
{
type: 'symbol',
path: '/CHANGELOG.md',
repository: 'test-repository',
repoStars: 4,
branches: ['a', 'b'],
commit: 'hunk12ef',
symbols: [
{
url: 'https://github.com/sourcegraph/sourcegraph',
name: 'TestSymbol',
containerName: 'TestContainer',
kind: SymbolKind.CONSTANT,
},
],
},
{
type: 'path',
path: '/CHANGELOG.md',
repository: 'test-repository',
repoStars: 5,
branches: ['a', 'b'],
commit: 'hunk12ef',
},
]
usePrependStyles('branded-story-styles', globalStyles)
useEffect(() => {
if (rootElementRef.current === null) {
return
}
applyTheme(isDarkTheme ? dark : light, rootElementRef.current)
}, [rootElementRef, isDarkTheme])
return (
<div ref={rootElementRef}>
<div className="d-flex justify-content-center">
<div className="mx-6">
<SearchResultList
matches={matches}
onPreviewChange={async () => {}}
onPreviewClear={async () => {}}
onOpen={async () => {}}
/>
</div>
</div>
</div>
)
}
JetBrainsSearchResultListStory.parameters = {
chromatic: {
disableSnapshot: false,
},
}

View File

@ -5,31 +5,31 @@ import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin'
import { remove } from 'lodash'
import signale from 'signale'
import SpeedMeasurePlugin from 'speed-measure-webpack-plugin'
import { DllReferencePlugin, Configuration, DefinePlugin, ProgressPlugin, RuleSetRule } from 'webpack'
import { Configuration, DefinePlugin, DllReferencePlugin, ProgressPlugin, RuleSetRule } from 'webpack'
import {
NODE_MODULES_PATH,
ROOT_PATH,
getBabelLoader,
getBasicCSSLoader,
getCacheConfig,
getCSSLoaders,
getCSSModulesLoader,
getCacheConfig,
getMonacoCSSRule,
getMonacoTTFRule,
getMonacoWebpackPlugin,
getProvidePlugin,
getTerserPlugin,
getBabelLoader,
getBasicCSSLoader,
getStatoscopePlugin,
getTerserPlugin,
NODE_MODULES_PATH,
ROOT_PATH,
STATIC_ASSETS_PATH,
} from '@sourcegraph/build-config'
import { ensureDllBundleIsReady } from './dllPlugin'
import { ENVIRONMENT_CONFIG } from './environment-config'
import {
monacoEditorPath,
dllPluginConfig,
dllBundleManifestPath,
dllPluginConfig,
monacoEditorPath,
readJsonFile,
storybookWorkspacePath,
} from './webpack.config.common'
@ -42,7 +42,7 @@ const getStoriesGlob = (): string[] => {
// Due to an issue with constant recompiling (https://github.com/storybookjs/storybook/issues/14342)
// we need to make the globs more specific (`(web|shared..)` also doesn't work). Once the above issue
// is fixed, this can be removed and watched for `client/**/*.story.tsx` again.
const directoriesWithStories = ['branded', 'browser', 'shared', 'web', 'wildcard', 'search-ui']
const directoriesWithStories = ['branded', 'browser', 'jetbrains/webview', 'shared', 'web', 'wildcard', 'search-ui']
const storiesGlobs = directoriesWithStories.map(packageDirectory =>
path.resolve(ROOT_PATH, `client/${packageDirectory}/src/**/*.story.tsx`)
)
@ -227,6 +227,23 @@ const config: Config = {
},
})
// Node.js polyfills for JetBrains plugin
config.module.rules.push({
test: /(?:client\/(?:shared|jetbrains)|node_modules\/https-browserify)\/.*\.(ts|tsx|js|jsx)$/,
resolve: {
alias: {
path: require.resolve('path-browserify'),
},
fallback: {
path: require.resolve('path-browserify'),
process: require.resolve('process/browser'),
util: require.resolve('util'),
http: require.resolve('stream-http'),
https: require.resolve('https-browserify'),
},
},
})
// Disable `CaseSensitivePathsPlugin` by default to speed up development build.
// Similar discussion: https://github.com/vercel/next.js/issues/6927#issuecomment-480579191
remove(config.plugins, plugin => plugin instanceof CaseSensitivePathsPlugin)

View File

@ -41,6 +41,7 @@
"storybook:dll": "yarn workspace @sourcegraph/storybook run start:dll",
"storybook:branded": "yarn workspace @sourcegraph/branded run storybook",
"storybook:browser": "yarn workspace @sourcegraph/browser run storybook",
"storybook:jetbrains": "yarn workspace @sourcegraph/jetbrains run storybook",
"storybook:shared": "yarn workspace @sourcegraph/shared run storybook",
"storybook:web": "yarn workspace @sourcegraph/web run storybook",
"storybook:search-ui": "yarn workspace @sourcegraph/search-ui run storybook",