Add analyzed repositories component

This adds the analyzed repositories component for showing within the
"Analyzed" tab. I wasn't completely sure whether there should be a
difference between "Pending" and "In progress", but pending will now not
show an icon, while in progress will show a spinner.

For the collapsible items, it does not reuse the `CollapsibleItem`
component because that component is tightly coupled with the styles
of the remote queries component.
This commit is contained in:
Koen Vlaswinkel
2022-09-27 14:59:12 +02:00
parent f408418f23
commit f8cc3aec32
14 changed files with 902 additions and 18 deletions

View File

@@ -1,4 +1,5 @@
import { Repository } from './repository';
import { AnalysisAlert, AnalysisRawResults } from './analysis-result';
export interface VariantAnalysis {
id: number,
@@ -78,6 +79,12 @@ export interface VariantAnalysisSkippedRepositoryGroup {
}>
}
export interface VariantAnalysisScannedRepositoryResult {
repositoryId: number;
interpretedResults?: AnalysisAlert[];
rawResults?: AnalysisRawResults;
}
/**
* Captures information needed to submit a variant
* analysis for processing.
@@ -102,16 +109,24 @@ export interface VariantAnalysisSubmission {
}
/**
* @param repo
* @returns whether the repo scan is in a completed state, i.e. it cannot normally change state anymore
* @param status
* @returns whether the status is in a completed state, i.e. it cannot normally change state anymore
*/
export function hasRepoScanCompleted(repo: VariantAnalysisScannedRepository): boolean {
export function isCompletedAnalysisRepoStatus(status: VariantAnalysisRepoStatus): boolean {
return [
// All states that indicates the repository has been scanned and cannot
// change status anymore.
VariantAnalysisRepoStatus.Succeeded, VariantAnalysisRepoStatus.Failed,
VariantAnalysisRepoStatus.Canceled, VariantAnalysisRepoStatus.TimedOut,
].includes(repo.analysisStatus);
].includes(status);
}
/**
* @param repo
* @returns whether the repo scan is in a completed state, i.e. it cannot normally change state anymore
*/
export function hasRepoScanCompleted(repo: VariantAnalysisScannedRepository): boolean {
return isCompletedAnalysisRepoStatus(repo.analysisStatus);
}
/**

View File

@@ -0,0 +1,26 @@
{
"schema": {
"name": "#select",
"rows": 1,
"columns": [
{
"kind": "i"
}
]
},
"resultSet": {
"schema": {
"name": "#select",
"rows": 1,
"columns": [
{
"kind": "i"
}
]
},
"rows": [[60688]]
},
"fileLinkPrefix": "https://github.com/facebook/create-react-app/blob/d960b9e38c062584ff6cfb1a70e1512509a966e7",
"sourceLocationPrefix": "/home/runner/work/bulk-builder/bulk-builder",
"capped": false
}

View File

@@ -0,0 +1,79 @@
import React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { VariantAnalysisContainer } from '../../view/variant-analysis/VariantAnalysisContainer';
import { VariantAnalysisAnalyzedRepoItem } from '../../view/variant-analysis/VariantAnalysisAnalyzedRepoItem';
import { VariantAnalysisRepoStatus } from '../../remote-queries/shared/variant-analysis';
import { AnalysisAlert, AnalysisRawResults } from '../../remote-queries/shared/analysis-result';
import analysesResults from '../remote-queries/data/analysesResultsMessage.json';
import rawResults from '../remote-queries/data/rawResults.json';
export default {
title: 'Variant Analysis/Analyzed Repo Item',
component: VariantAnalysisAnalyzedRepoItem,
decorators: [
(Story) => (
<VariantAnalysisContainer>
<Story />
</VariantAnalysisContainer>
)
],
} as ComponentMeta<typeof VariantAnalysisAnalyzedRepoItem>;
const Template: ComponentStory<typeof VariantAnalysisAnalyzedRepoItem> = (args) => (
<VariantAnalysisAnalyzedRepoItem {...args} />
);
export const Pending = Template.bind({});
Pending.args = {
repository: {
id: 63537249,
fullName: 'facebook/create-react-app',
private: false,
},
status: VariantAnalysisRepoStatus.Pending,
};
export const InProgress = Template.bind({});
InProgress.args = {
...Pending.args,
status: VariantAnalysisRepoStatus.InProgress,
interpretedResults: undefined,
};
export const Failed = Template.bind({});
Failed.args = {
...Pending.args,
status: VariantAnalysisRepoStatus.Failed,
interpretedResults: undefined,
};
export const TimedOut = Template.bind({});
TimedOut.args = {
...Pending.args,
status: VariantAnalysisRepoStatus.TimedOut,
};
export const Canceled = Template.bind({});
Canceled.args = {
...Pending.args,
status: VariantAnalysisRepoStatus.Canceled,
};
export const InterpretedResults = Template.bind({});
InterpretedResults.args = {
...Pending.args,
status: VariantAnalysisRepoStatus.Succeeded,
resultCount: 198,
interpretedResults: analysesResults.analysesResults.find(v => v.nwo === 'facebook/create-react-app')?.interpretedResults as unknown as AnalysisAlert[],
};
export const RawResults = Template.bind({});
RawResults.args = {
...InterpretedResults.args,
interpretedResults: undefined,
resultCount: 1,
rawResults: rawResults as unknown as AnalysisRawResults,
};

View File

@@ -0,0 +1,116 @@
import React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { VariantAnalysisContainer } from '../../view/variant-analysis/VariantAnalysisContainer';
import { VariantAnalysisAnalyzedRepos } from '../../view/variant-analysis/VariantAnalysisAnalyzedRepos';
import {
VariantAnalysisQueryLanguage,
VariantAnalysisRepoStatus,
VariantAnalysisStatus
} from '../../remote-queries/shared/variant-analysis';
import { AnalysisAlert } from '../../remote-queries/shared/analysis-result';
import analysesResults from '../remote-queries/data/analysesResultsMessage.json';
export default {
title: 'Variant Analysis/Analyzed Repos',
component: VariantAnalysisAnalyzedRepos,
decorators: [
(Story) => (
<VariantAnalysisContainer>
<Story />
</VariantAnalysisContainer>
)
],
} as ComponentMeta<typeof VariantAnalysisAnalyzedRepos>;
const Template: ComponentStory<typeof VariantAnalysisAnalyzedRepos> = (args) => (
<VariantAnalysisAnalyzedRepos {...args} />
);
const interpretedResultsForRepo = (nwo: string): AnalysisAlert[] | undefined => {
return analysesResults.analysesResults.find(v => v.nwo === nwo)?.interpretedResults as unknown as AnalysisAlert[];
};
export const Example = Template.bind({});
Example.args = {
variantAnalysis: {
id: 1,
controllerRepoId: 1,
query: {
name: 'Query name',
filePath: 'example.ql',
language: VariantAnalysisQueryLanguage.Javascript,
},
databases: {},
status: VariantAnalysisStatus.InProgress,
scannedRepos: [
{
repository: {
id: 63537249,
fullName: 'facebook/create-react-app',
private: false,
},
analysisStatus: VariantAnalysisRepoStatus.Succeeded, resultCount: 198,
},
{
repository: {
id: 167174,
fullName: 'jquery/jquery',
private: false,
},
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
resultCount: 67,
},
{
repository: {
id: 237159,
fullName: 'expressjs/express',
private: false,
},
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
resultCount: 26,
},
{
repository: {
id: 15062869,
fullName: 'facebook/jest',
private: false,
},
analysisStatus: VariantAnalysisRepoStatus.Failed,
},
{
repository: {
id: 24195339,
fullName: 'angular/angular',
private: false,
},
analysisStatus: VariantAnalysisRepoStatus.InProgress,
},
{
repository: {
id: 24560307,
fullName: 'babel/babel',
private: false,
},
analysisStatus: VariantAnalysisRepoStatus.Pending,
},
]
},
repositoryResults: [
{
repositoryId: 63537249,
interpretedResults: interpretedResultsForRepo('facebook/create-react-app'),
},
{
repositoryId: 167174,
interpretedResults: interpretedResultsForRepo('jquery/jquery'),
},
{
repositoryId: 237159,
interpretedResults: interpretedResultsForRepo('expressjs/express'),
}
]
}
;

View File

@@ -0,0 +1,13 @@
import * as React from 'react';
import { Codicon } from './Codicon';
import classNames from 'classnames';
type Props = {
label?: string;
className?: string;
}
export const LoadingIcon = ({
label = 'Loading...',
className,
}: Props) => <Codicon name="loading" label={label} className={classNames(className, 'codicon-modifier-spin')} />;

View File

@@ -1,4 +1,5 @@
export * from './Codicon';
export * from './ErrorIcon';
export * from './LoadingIcon';
export * from './SuccessIcon';
export * from './WarningIcon';

View File

@@ -0,0 +1,88 @@
import * as React from 'react';
import styled from 'styled-components';
import { AnalysisAlert, AnalysisRawResults } from '../../remote-queries/shared/analysis-result';
import AnalysisAlertResult from '../remote-queries/AnalysisAlertResult';
import RawResultsTable from '../remote-queries/RawResultsTable';
import { VariantAnalysisRepoStatus } from '../../remote-queries/shared/variant-analysis';
import { Alert } from '../common';
const ContentContainer = styled.div`
display: flex;
flex-direction: column;
`;
const AlertContainer = styled.div`
margin-top: 1em;
`;
const InterpretedResultsContainer = styled.ul`
list-style-type: none;
margin: 1em 0 0;
padding: 0.5em 0 0 0;
`;
const InterpretedResultItem = styled.li`
margin-bottom: 1em;
background-color: var(--vscode-notifications-background);
`;
const RawResultsContainer = styled.div`
display: block;
margin-top: 0.5em;
`;
export type AnalyzedRepoItemContentProps = {
status: VariantAnalysisRepoStatus;
interpretedResults?: AnalysisAlert[];
rawResults?: AnalysisRawResults;
}
export const AnalyzedRepoItemContent = ({
status,
interpretedResults,
rawResults,
}: AnalyzedRepoItemContentProps) => {
return (
<ContentContainer>
{status === VariantAnalysisRepoStatus.Failed && <AlertContainer>
<Alert
type="error"
title="Failed"
message="The query failed to run on this repository."
/>
</AlertContainer>}
{status === VariantAnalysisRepoStatus.TimedOut && <AlertContainer>
<Alert
type="error"
title="Timed out"
message="The analysis ran out of time and we couldn't scan the repository."
/>
</AlertContainer>}
{status === VariantAnalysisRepoStatus.Canceled && <AlertContainer>
<Alert
type="error"
title="Canceled"
message="The variant analysis or this repository was canceled."
/>
</AlertContainer>}
{interpretedResults && (
<InterpretedResultsContainer>
{interpretedResults.map((r, i) =>
<InterpretedResultItem key={i}>
<AnalysisAlertResult alert={r} />
</InterpretedResultItem>)}
</InterpretedResultsContainer>
)}
{rawResults && (
<RawResultsContainer>
<RawResultsTable
schema={rawResults.schema}
results={rawResults.resultSet}
fileLinkPrefix={rawResults.fileLinkPrefix}
sourceLocationPrefix={rawResults.sourceLocationPrefix} />
</RawResultsContainer>
)}
</ContentContainer>
);
};

View File

@@ -3,7 +3,7 @@ import * as React from 'react';
import {
VariantAnalysis as VariantAnalysisDomainModel,
VariantAnalysisQueryLanguage,
VariantAnalysisRepoStatus,
VariantAnalysisRepoStatus, VariantAnalysisScannedRepositoryResult,
VariantAnalysisStatus
} from '../../remote-queries/shared/variant-analysis';
import { VariantAnalysisContainer } from './VariantAnalysisContainer';
@@ -29,7 +29,7 @@ const variantAnalysis: VariantAnalysisDomainModel = {
fullName: 'octodemo/hello-world-1',
private: false,
},
analysisStatus: VariantAnalysisRepoStatus.Pending,
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
},
{
repository: {
@@ -37,7 +37,7 @@ const variantAnalysis: VariantAnalysisDomainModel = {
fullName: 'octodemo/hello-world-2',
private: false,
},
analysisStatus: VariantAnalysisRepoStatus.Pending,
analysisStatus: VariantAnalysisRepoStatus.Canceled,
},
{
repository: {
@@ -45,7 +45,7 @@ const variantAnalysis: VariantAnalysisDomainModel = {
fullName: 'octodemo/hello-world-3',
private: false,
},
analysisStatus: VariantAnalysisRepoStatus.Pending,
analysisStatus: VariantAnalysisRepoStatus.TimedOut,
},
{
repository: {
@@ -53,7 +53,7 @@ const variantAnalysis: VariantAnalysisDomainModel = {
fullName: 'octodemo/hello-world-4',
private: false,
},
analysisStatus: VariantAnalysisRepoStatus.Pending,
analysisStatus: VariantAnalysisRepoStatus.Failed,
},
{
repository: {
@@ -61,7 +61,7 @@ const variantAnalysis: VariantAnalysisDomainModel = {
fullName: 'octodemo/hello-world-5',
private: false,
},
analysisStatus: VariantAnalysisRepoStatus.Pending,
analysisStatus: VariantAnalysisRepoStatus.InProgress,
},
{
repository: {
@@ -69,7 +69,7 @@ const variantAnalysis: VariantAnalysisDomainModel = {
fullName: 'octodemo/hello-world-6',
private: false,
},
analysisStatus: VariantAnalysisRepoStatus.Pending,
analysisStatus: VariantAnalysisRepoStatus.InProgress,
},
{
repository: {
@@ -158,6 +158,42 @@ const variantAnalysis: VariantAnalysisDomainModel = {
},
};
const repositoryResults: VariantAnalysisScannedRepositoryResult[] = [
{
repositoryId: 1,
rawResults: {
schema: {
name: '#select',
rows: 1,
columns: [
{
kind: 'i'
}
]
},
resultSet: {
schema: {
name: '#select',
rows: 1,
columns: [
{
kind: 'i'
}
]
},
rows: [
[
60688
]
]
},
fileLinkPrefix: 'https://github.com/octodemo/hello-world-1/blob/59a2a6c7d9dde7a6ecb77c2f7e8197d6925c143b',
sourceLocationPrefix: '/home/runner/work/bulk-builder/bulk-builder',
capped: false
}
}
];
function getContainerContents(variantAnalysis: VariantAnalysisDomainModel) {
if (variantAnalysis.actionsWorkflowRunId === undefined) {
return <VariantAnalysisLoading />;
@@ -174,7 +210,10 @@ function getContainerContents(variantAnalysis: VariantAnalysisDomainModel) {
onExportResultsClick={() => console.log('Export results')}
onViewLogsClick={() => console.log('View logs')}
/>
<VariantAnalysisOutcomePanels variantAnalysis={variantAnalysis} />
<VariantAnalysisOutcomePanels
variantAnalysis={variantAnalysis}
repositoryResults={repositoryResults}
/>
</>
);
}

View File

@@ -0,0 +1,90 @@
import * as React from 'react';
import { useCallback, useState } from 'react';
import styled from 'styled-components';
import { VSCodeBadge } from '@vscode/webview-ui-toolkit/react';
import { isCompletedAnalysisRepoStatus, VariantAnalysisRepoStatus } from '../../remote-queries/shared/variant-analysis';
import { formatDecimal } from '../../pure/number';
import { Codicon, ErrorIcon, LoadingIcon, SuccessIcon } from '../common';
import { Repository } from '../../remote-queries/shared/repository';
import { AnalysisAlert, AnalysisRawResults } from '../../remote-queries/shared/analysis-result';
import { AnalyzedRepoItemContent } from './AnalyzedRepoItemContent';
// This will ensure that these icons have a className which we can use in the TitleContainer
const ExpandCollapseCodicon = styled(Codicon)``;
const TitleContainer = styled.button`
display: flex;
gap: 0.5em;
align-items: center;
color: var(--vscode-editor-foreground);
background-color: transparent;
border: none;
cursor: pointer;
&:disabled {
cursor: default;
${ExpandCollapseCodicon} {
color: var(--vscode-disabledForeground);
}
}
`;
const Visibility = styled.span`
font-size: 0.85em;
color: var(--vscode-descriptionForeground);
`;
export type VariantAnalysisAnalyzedRepoItemProps = {
repository: Repository;
status: VariantAnalysisRepoStatus;
resultCount?: number;
interpretedResults?: AnalysisAlert[];
rawResults?: AnalysisRawResults;
}
const getErrorLabel = (status: VariantAnalysisRepoStatus.Failed | VariantAnalysisRepoStatus.TimedOut | VariantAnalysisRepoStatus.Canceled): string => {
switch (status) {
case VariantAnalysisRepoStatus.Failed:
return 'Failed';
case VariantAnalysisRepoStatus.TimedOut:
return 'Timed out';
case VariantAnalysisRepoStatus.Canceled:
return 'Canceled';
}
};
export const VariantAnalysisAnalyzedRepoItem = ({
repository,
status,
resultCount,
interpretedResults,
rawResults,
}: VariantAnalysisAnalyzedRepoItemProps) => {
const [isExpanded, setExpanded] = useState(false);
const toggleExpanded = useCallback(() => {
setExpanded(oldIsExpanded => !oldIsExpanded);
}, []);
const disabled = !isCompletedAnalysisRepoStatus(status);
return (
<div>
<TitleContainer onClick={toggleExpanded} disabled={disabled} aria-expanded={isExpanded}>
{isExpanded ? <ExpandCollapseCodicon name="chevron-down" label="Collapse" /> : <ExpandCollapseCodicon name="chevron-right" label="Expand" />}
<VSCodeBadge>{resultCount === undefined ? '-' : formatDecimal(resultCount)}</VSCodeBadge>
<span>{repository.fullName}</span>
<Visibility>{repository.private ? 'private' : 'public'}</Visibility>
<span>
{status === VariantAnalysisRepoStatus.Succeeded && <SuccessIcon />}
{(status === VariantAnalysisRepoStatus.Failed || status === VariantAnalysisRepoStatus.TimedOut || status === VariantAnalysisRepoStatus.Canceled) && <ErrorIcon label={getErrorLabel(status)} />}
{status === VariantAnalysisRepoStatus.InProgress && <LoadingIcon label="In progress" />}
</span>
</TitleContainer>
{isExpanded && <AnalyzedRepoItemContent status={status} interpretedResults={interpretedResults} rawResults={rawResults} />}
</div>
);
};

View File

@@ -1,5 +1,48 @@
import * as React from 'react';
import styled from 'styled-components';
import { VariantAnalysis, VariantAnalysisScannedRepositoryResult } from '../../remote-queries/shared/variant-analysis';
import { VariantAnalysisAnalyzedRepoItem } from './VariantAnalysisAnalyzedRepoItem';
import { useMemo } from 'react';
export const VariantAnalysisAnalyzedRepos = () => {
return <div>This is the analyzed view</div>;
const Container = styled.div`
display: flex;
flex-direction: column;
gap: 1em;
`;
export type VariantAnalysisAnalyzedReposProps = {
variantAnalysis: VariantAnalysis;
repositoryResults?: VariantAnalysisScannedRepositoryResult[];
}
export const VariantAnalysisAnalyzedRepos = ({
variantAnalysis,
repositoryResults,
}: VariantAnalysisAnalyzedReposProps) => {
const repositoryResultsById = useMemo(() => {
const map = new Map<number, VariantAnalysisScannedRepositoryResult>();
repositoryResults?.forEach((repository) => {
map.set(repository.repositoryId, repository);
});
return map;
}, [repositoryResults]);
return (
<Container>
{variantAnalysis.scannedRepos?.map(repository => {
const results = repositoryResultsById.get(repository.repository.id);
return (
<VariantAnalysisAnalyzedRepoItem
key={repository.repository.id}
repository={repository.repository}
status={repository.analysisStatus}
resultCount={repository.resultCount}
interpretedResults={results?.interpretedResults}
rawResults={results?.rawResults}
/>
);
})}
</Container>
);
};

View File

@@ -2,7 +2,7 @@ import * as React from 'react';
import styled from 'styled-components';
import { VSCodeBadge, VSCodePanels, VSCodePanelTab, VSCodePanelView } from '@vscode/webview-ui-toolkit/react';
import { formatDecimal } from '../../pure/number';
import { VariantAnalysis } from '../../remote-queries/shared/variant-analysis';
import { VariantAnalysis, VariantAnalysisScannedRepositoryResult } from '../../remote-queries/shared/variant-analysis';
import { VariantAnalysisAnalyzedRepos } from './VariantAnalysisAnalyzedRepos';
import { VariantAnalysisNotFoundRepos } from './VariantAnalysisNotFoundRepos';
import { VariantAnalysisNoCodeqlDbRepos } from './VariantAnalysisNoCodeqlDbRepos';
@@ -10,6 +10,7 @@ import { Alert } from '../common';
export type VariantAnalysisOutcomePanelProps = {
variantAnalysis: VariantAnalysis;
repositoryResults?: VariantAnalysisScannedRepositoryResult[];
};
const Tab = styled(VSCodePanelTab)`
@@ -33,7 +34,8 @@ const WarningsContainer = styled.div`
`;
export const VariantAnalysisOutcomePanels = ({
variantAnalysis
variantAnalysis,
repositoryResults,
}: VariantAnalysisOutcomePanelProps) => {
const noCodeqlDbRepositoryCount = variantAnalysis.skippedRepos?.noCodeqlDbRepos?.repositoryCount ?? 0;
const notFoundRepositoryCount = variantAnalysis.skippedRepos?.notFoundRepos?.repositoryCount ?? 0;
@@ -63,7 +65,7 @@ export const VariantAnalysisOutcomePanels = ({
return (
<>
{warnings}
<VariantAnalysisAnalyzedRepos />
<VariantAnalysisAnalyzedRepos variantAnalysis={variantAnalysis} repositoryResults={repositoryResults} />
</>
);
}
@@ -88,7 +90,7 @@ export const VariantAnalysisOutcomePanels = ({
<VSCodeBadge appearance="secondary">{formatDecimal(noCodeqlDbRepositoryCount)}</VSCodeBadge>
</Tab>
)}
<VSCodePanelView><VariantAnalysisAnalyzedRepos /></VSCodePanelView>
<VSCodePanelView><VariantAnalysisAnalyzedRepos variantAnalysis={variantAnalysis} repositoryResults={repositoryResults} /></VSCodePanelView>
{notFoundRepositoryCount > 0 && <VSCodePanelView><VariantAnalysisNotFoundRepos /></VSCodePanelView>}
{noCodeqlDbRepositoryCount > 0 && <VSCodePanelView><VariantAnalysisNoCodeqlDbRepos /></VSCodePanelView>}
</VSCodePanels>

View File

@@ -0,0 +1,115 @@
import * as React from 'react';
import { render as reactRender, screen } from '@testing-library/react';
import { VariantAnalysisRepoStatus } from '../../../remote-queries/shared/variant-analysis';
import { AnalyzedRepoItemContent, AnalyzedRepoItemContentProps } from '../AnalyzedRepoItemContent';
describe(AnalyzedRepoItemContent.name, () => {
const render = (props: Partial<AnalyzedRepoItemContentProps> = {}) => {
return reactRender(
<AnalyzedRepoItemContent
status={VariantAnalysisRepoStatus.Succeeded}
{...props}
/>
);
};
it('renders the succeeded state with interpreted results', () => {
render({
status: VariantAnalysisRepoStatus.Succeeded,
interpretedResults: [
{
message: {
tokens: [
{
t: 'text',
text: 'This is an empty block.'
}
]
},
shortDescription: 'This is an empty block.',
fileLink: {
fileLinkPrefix: 'https://github.com/facebook/create-react-app/blob/f34d88e30c7d8be7181f728d1abc4fd8d5cd07d3',
filePath: 'packages/create-react-app/createReactApp.js'
},
severity: 'Warning',
codeSnippet: {
startLine: 655,
endLine: 662,
text: ' try {\n callback();\n } catch (ignored) {\n // Callback might throw and fail, since it\'s a temp directory the\n // OS will clean it up eventually...\n }\n },\n });\n'
},
highlightedRegion: {
startLine: 657,
startColumn: 31,
endLine: 660,
endColumn: 14
},
codeFlows: []
}
]
});
expect(screen.getByText('This is an empty block.')).toBeInTheDocument();
});
it('renders the succeeded state with raw results', () => {
render({
status: VariantAnalysisRepoStatus.Succeeded,
rawResults: {
schema: {
name: '#select',
rows: 1,
columns: [
{
kind: 'i'
}
]
},
resultSet: {
schema: {
name: '#select',
rows: 1,
columns: [
{
kind: 'i'
}
]
},
rows: [
[
60688
]
]
},
fileLinkPrefix: 'https://github.com/octodemo/hello-world-1/blob/59a2a6c7d9dde7a6ecb77c2f7e8197d6925c143b',
sourceLocationPrefix: '/home/runner/work/bulk-builder/bulk-builder',
capped: false
}
});
expect(screen.getByText('60688')).toBeInTheDocument();
});
it('renders the failed state', () => {
render({
status: VariantAnalysisRepoStatus.Failed,
});
expect(screen.getByText('Error: Failed')).toBeInTheDocument();
});
it('renders the timed out state', () => {
render({
status: VariantAnalysisRepoStatus.TimedOut,
});
expect(screen.getByText('Error: Timed out')).toBeInTheDocument();
});
it('renders the canceled state', () => {
render({
status: VariantAnalysisRepoStatus.Canceled,
});
expect(screen.getByText('Error: Canceled')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,146 @@
import * as React from 'react';
import { render as reactRender, screen } from '@testing-library/react';
import { VariantAnalysisRepoStatus } from '../../../remote-queries/shared/variant-analysis';
import {
VariantAnalysisAnalyzedRepoItem,
VariantAnalysisAnalyzedRepoItemProps
} from '../VariantAnalysisAnalyzedRepoItem';
import userEvent from '@testing-library/user-event';
describe(VariantAnalysisAnalyzedRepoItem.name, () => {
const render = (props: Partial<VariantAnalysisAnalyzedRepoItemProps> = {}) => {
return reactRender(
<VariantAnalysisAnalyzedRepoItem
repository={{
id: 1,
fullName: 'octodemo/hello-world-1',
private: false,
}}
status={VariantAnalysisRepoStatus.Pending}
{...props}
/>
);
};
it('renders the pending state', () => {
render();
expect(screen.getByText('octodemo/hello-world-1')).toBeInTheDocument();
expect(screen.getByText('-')).toBeInTheDocument();
expect(screen.queryByRole('img', {
// There should not be any icons, except the expand icon
name: (name) => name.toLowerCase() !== 'expand',
})).not.toBeInTheDocument();
expect(screen.getByRole<HTMLButtonElement>('button', {
expanded: false
})).toBeDisabled();
});
it('renders the in progress state', () => {
render({
status: VariantAnalysisRepoStatus.InProgress,
});
expect(screen.getByRole('img', {
name: 'In progress',
})).toBeInTheDocument();
expect(screen.getByRole<HTMLButtonElement>('button', {
expanded: false
})).toBeDisabled();
});
it('renders the succeeded state', () => {
render({
status: VariantAnalysisRepoStatus.Succeeded,
resultCount: 178,
});
expect(screen.getByRole('img', {
name: 'Success',
})).toBeInTheDocument();
expect(screen.getByText('178')).toBeInTheDocument();
expect(screen.getByRole<HTMLButtonElement>('button', {
expanded: false
})).toBeEnabled();
});
it('renders the failed state', () => {
render({
status: VariantAnalysisRepoStatus.Failed,
});
expect(screen.getByRole('img', {
name: 'Failed',
})).toBeInTheDocument();
expect(screen.getByRole<HTMLButtonElement>('button', {
expanded: false
})).toBeEnabled();
});
it('renders the timed out state', () => {
render({
status: VariantAnalysisRepoStatus.TimedOut,
});
expect(screen.getByRole('img', {
name: 'Timed out',
})).toBeInTheDocument();
expect(screen.getByRole<HTMLButtonElement>('button', {
expanded: false
})).toBeEnabled();
});
it('renders the canceled state', () => {
render({
status: VariantAnalysisRepoStatus.Canceled,
});
expect(screen.getByRole('img', {
name: 'Canceled',
})).toBeInTheDocument();
expect(screen.getByRole<HTMLButtonElement>('button', {
expanded: false
})).toBeEnabled();
});
it('shows the repo as public', () => {
render({
repository: {
id: 1,
fullName: 'octodemo/hello-world-1',
private: false,
}
});
expect(screen.getByText('public')).toBeInTheDocument();
});
it('shows the repo as private', () => {
render({
repository: {
id: 1,
fullName: 'octodemo/hello-world-1',
private: true,
}
});
expect(screen.getByText('private')).toBeInTheDocument();
});
it('can expand the repo item', async () => {
render({
status: VariantAnalysisRepoStatus.TimedOut,
});
await userEvent.click(screen.getByRole('button', {
expanded: false
}));
screen.getByRole('button', {
expanded: true,
});
screen.getByText('Error: Timed out');
});
});

View File

@@ -0,0 +1,111 @@
import * as React from 'react';
import { render as reactRender, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
VariantAnalysisQueryLanguage,
VariantAnalysisRepoStatus,
VariantAnalysisStatus
} from '../../../remote-queries/shared/variant-analysis';
import { VariantAnalysisAnalyzedRepos, VariantAnalysisAnalyzedReposProps } from '../VariantAnalysisAnalyzedRepos';
describe(VariantAnalysisAnalyzedRepos.name, () => {
const defaultVariantAnalysis = {
id: 1,
controllerRepoId: 1,
actionsWorkflowRunId: 789263,
query: {
name: 'Example query',
filePath: 'example.ql',
language: VariantAnalysisQueryLanguage.Javascript,
},
databases: {},
status: VariantAnalysisStatus.InProgress,
scannedRepos: [
{
repository: {
id: 1,
fullName: 'octodemo/hello-world-1',
private: false,
},
analysisStatus: VariantAnalysisRepoStatus.Pending,
},
{
repository: {
id: 2,
fullName: 'octodemo/hello-world-2',
private: false,
},
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
},
{
repository: {
id: 3,
fullName: 'octodemo/hello-world-3',
private: true,
},
analysisStatus: VariantAnalysisRepoStatus.Failed,
},
{
repository: {
id: 4,
fullName: 'octodemo/hello-world-4',
private: false,
},
analysisStatus: VariantAnalysisRepoStatus.InProgress,
},
],
};
const render = (props: Partial<VariantAnalysisAnalyzedReposProps> = {}) => {
return reactRender(
<VariantAnalysisAnalyzedRepos
variantAnalysis={defaultVariantAnalysis}
{...props}
/>
);
};
it('renders the repository names', () => {
render();
expect(screen.getByText('octodemo/hello-world-1')).toBeInTheDocument();
expect(screen.getByText('octodemo/hello-world-2')).toBeInTheDocument();
expect(screen.getByText('octodemo/hello-world-3')).toBeInTheDocument();
expect(screen.getByText('octodemo/hello-world-4')).toBeInTheDocument();
});
it('renders the interpreted result for a succeeded repo', async () => {
render({
repositoryResults: [
{
repositoryId: 2,
interpretedResults: [
{
message: {
tokens: [
{
t: 'text',
text: 'This is an empty block.'
}
]
},
shortDescription: 'This is an empty block.',
fileLink: {
fileLinkPrefix: 'https://github.com/facebook/create-react-app/blob/f34d88e30c7d8be7181f728d1abc4fd8d5cd07d3',
filePath: 'packages/create-react-app/createReactApp.js'
},
severity: 'Warning',
codeFlows: []
}
],
}
]
});
expect(screen.queryByText('This is an empty block.')).not.toBeInTheDocument();
await userEvent.click(screen.getByRole('button', {
name: /octodemo\/hello-world-2/,
}));
expect(screen.getByText('This is an empty block.')).toBeInTheDocument();
});
});