Replace useQuery & getNewQueryParams with single hook useQueryHandler

Add tests for isProjectValid & useQueryHandler

Signed-off-by: Erik Engervall <erik.engervall@gmail.com>
This commit is contained in:
Erik Engervall
2021-04-18 11:00:51 +02:00
parent 0962884ebd
commit 28b784d270
11 changed files with 227 additions and 113 deletions
@@ -32,18 +32,17 @@ import {
import { CenteredCircularProgress } from './components/CenteredCircularProgress';
import { CreateRc } from './cards/createRc/CreateRc';
import { getGitHubBatchInfo } from './sideEffects/getGitHubBatchInfo';
import { getParsedQuery } from './helpers/getNewQueryParams';
import { githubReleaseManagerApiRef } from './api/serviceApiRef';
import { Info } from './cards/info/Info';
import { InfoCardPlus } from './components/InfoCardPlus';
import { isProjectValid } from './cards/projectForm/isProjectValid';
import { isProjectValid } from './helpers/isProjectValid';
import { Patch } from './cards/patchRc/Patch';
import { ProjectContext, Project } from './contexts/ProjectContext';
import { PromoteRc } from './cards/promoteRc/PromoteRc';
import { RefetchContext } from './contexts/RefetchContext';
import { RepoDetailsForm } from './cards/projectForm/RepoDetailsForm';
import { useQuery } from './helpers/useQuery';
import { useVersioningStrategyMatchesRepoTags } from './helpers/useVersioningStrategyMatchesRepoTags';
import { useQueryHandler } from './helpers/useQueryHandler';
interface GitHubReleaseManagerProps {
components?: {
@@ -66,13 +65,15 @@ export function GitHubReleaseManager({
}: GitHubReleaseManagerProps) {
const pluginApiClient = useApi(githubReleaseManagerApiRef);
const classes = useStyles();
const query = useQuery();
const parsedQuery = getParsedQuery({ query });
const { getParsedQuery } = useQueryHandler();
const { parsedQuery } = getParsedQuery();
const project: Project = {
owner: parsedQuery.owner ?? '',
repo: parsedQuery.repo ?? '',
versioningStrategy: parsedQuery.versioningStrategy ?? 'semver',
};
const usernameResponse = useAsync(() =>
pluginApiClient.getUsername({ owner: project.owner, repo: project.repo }),
);
@@ -157,19 +158,23 @@ function Cards({
);
}
if (!versioningStrategyMatches) {
return (
<Alert severity="error">
Versioning mismatch, expected {project.versioningStrategy} version, got{' '}
{gitHubBatchInfo.value.latestRelease?.tagName}
</Alert>
);
}
return (
<ProjectContext.Provider value={project}>
<RefetchContext.Provider value={{ refetchTrigger, setRefetchTrigger }}>
<ErrorBoundary>
{gitHubBatchInfo.value.latestRelease && !versioningStrategyMatches && (
<Alert severity="warning" style={{ marginBottom: 20 }}>
Versioning mismatch, expected {project.versioningStrategy}{' '}
version, got "{gitHubBatchInfo.value.latestRelease.tagName}"
</Alert>
)}
{!gitHubBatchInfo.value.latestRelease && (
<Alert severity="info" style={{ marginBottom: 20 }}>
This repository has not releases yet
</Alert>
)}
<Info
latestRelease={gitHubBatchInfo.value.latestRelease}
releaseBranch={gitHubBatchInfo.value.releaseBranch}
@@ -25,12 +25,11 @@ import {
Select,
} from '@material-ui/core';
import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext';
import { useFormClasses } from './styles';
import { CenteredCircularProgress } from '../../components/CenteredCircularProgress';
import { Project } from '../../contexts/ProjectContext';
import { getNewQueryParams } from '../../helpers/getNewQueryParams';
import { useQuery } from '../../helpers/useQuery';
import { useFormClasses } from './styles';
import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext';
import { useQueryHandler } from '../../helpers/useQueryHandler';
export function Owner({
username,
@@ -39,10 +38,10 @@ export function Owner({
username: string;
project: Project;
}) {
const pluginApiClient = usePluginApiClientContext();
const formClasses = useFormClasses();
const navigate = useNavigate();
const query = useQuery();
const pluginApiClient = usePluginApiClientContext();
const { getQueryParamsWithUpdates } = useQueryHandler();
const { loading, error, value } = useAsync(() => pluginApiClient.getOwners());
const owners = value?.owners ?? [];
@@ -63,8 +62,7 @@ export function Owner({
value={project.owner}
defaultValue=""
onChange={event => {
const queryParams = getNewQueryParams({
query,
const { queryParams } = getQueryParamsWithUpdates({
updates: [
{ key: 'repo', value: '' },
{ key: 'owner', value: event.target.value as string },
@@ -29,14 +29,13 @@ import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext
import { useFormClasses } from './styles';
import { CenteredCircularProgress } from '../../components/CenteredCircularProgress';
import { Project } from '../../contexts/ProjectContext';
import { getNewQueryParams } from '../../helpers/getNewQueryParams';
import { useQuery } from '../../helpers/useQuery';
import { useQueryHandler } from '../../helpers/useQueryHandler';
export function Repo({ project }: { project: Project }) {
const pluginApiClient = usePluginApiClientContext();
const navigate = useNavigate();
const formClasses = useFormClasses();
const query = useQuery();
const { getQueryParamsWithUpdates } = useQueryHandler();
const { loading, error, value } = useAsync(
async () => pluginApiClient.getRepositories({ owner: project.owner }),
@@ -59,8 +58,7 @@ export function Repo({ project }: { project: Project }) {
value={project.repo}
defaultValue=""
onChange={event => {
const queryParams = getNewQueryParams({
query,
const { queryParams } = getQueryParamsWithUpdates({
updates: [{ key: 'repo', value: event.target.value as string }],
});
@@ -25,22 +25,17 @@ import {
} from '@material-ui/core';
import { Project } from '../../contexts/ProjectContext';
import { useQuery } from '../../helpers/useQuery';
import {
getNewQueryParams,
getParsedQuery,
} from '../../helpers/getNewQueryParams';
import { useQueryHandler } from '../../helpers/useQueryHandler';
export function VersioningStrategy({ project }: { project: Project }) {
const navigate = useNavigate();
const query = useQuery();
const { getParsedQuery, getQueryParamsWithUpdates } = useQueryHandler();
useEffect(() => {
const parsedQuery = getParsedQuery({ query });
const { parsedQuery } = getParsedQuery();
if (!parsedQuery.versioningStrategy) {
const queryParams = getNewQueryParams({
query,
const { queryParams } = getQueryParamsWithUpdates({
updates: [
{ key: 'versioningStrategy', value: project.versioningStrategy },
],
@@ -59,8 +54,7 @@ export function VersioningStrategy({ project }: { project: Project }) {
value={project.versioningStrategy}
defaultValue="semver"
onChange={event => {
const queryParams = getNewQueryParams({
query,
const queryParams = getQueryParamsWithUpdates({
updates: [{ key: 'versioningStrategy', value: event.target.value }],
});
@@ -1,44 +0,0 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import qs from 'qs';
import { Project } from '../contexts/ProjectContext';
export function getParsedQuery({ query }: { query: URLSearchParams }) {
const parsedQuery: Partial<Project> = qs.parse(query.toString());
return parsedQuery;
}
export function getNewQueryParams({
query,
updates,
}: {
query: URLSearchParams;
updates: {
key: keyof Project;
value: string;
}[];
}) {
const queryParams = qs.parse(query.toString());
for (const { key, value } of updates) {
queryParams[key] = value;
}
return qs.stringify(queryParams);
}
@@ -0,0 +1,47 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { mockSemverProject } from '../test-helpers/test-helpers';
import { isProjectValid } from './isProjectValid';
describe('isProjectValid', () => {
it('should return true for valid project', () => {
const result = isProjectValid(mockSemverProject);
expect(result).toEqual(true);
});
it('should return false for invalid project (undefined argument)', () => {
const result = isProjectValid(undefined);
expect(result).toEqual(false);
});
it('should return false for invalid project (empty object argument)', () => {
const result = isProjectValid({});
expect(result).toEqual(false);
});
it('should return false for invalid project (invalid versioningStrategy argument)', () => {
const result = isProjectValid({
...mockSemverProject,
versioningStrategy: 'banana',
});
expect(result).toEqual(false);
});
});
@@ -14,12 +14,14 @@
* limitations under the License.
*/
import { Project } from '../../contexts/ProjectContext';
import { Project } from '../contexts/ProjectContext';
export function isProjectValid(project: any): project is Project {
return (
project?.owner?.length > 0 &&
project?.repo?.length > 0 &&
project?.versioningStrategy?.length > 0
(['semver', 'calver'] as Project['versioningStrategy'][]).includes(
project?.versioningStrategy,
)
);
}
@@ -1,21 +0,0 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useLocation } from 'react-router';
export function useQuery(): URLSearchParams {
return new URLSearchParams(useLocation().search);
}
@@ -0,0 +1,64 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { render } from '@testing-library/react';
import { mockSemverProject } from '../test-helpers/test-helpers';
jest.mock('react-router', () => ({
useLocation: jest.fn(() => ({
search: `?versioningStrategy=${mockSemverProject.versioningStrategy}&owner=${mockSemverProject.owner}&repo=${mockSemverProject.repo}`,
})),
}));
import { useQueryHandler } from './useQueryHandler';
const TEST_ID = 'grm--use-query-handler';
const MockComponent = () => {
const { getParsedQuery, getQueryParamsWithUpdates } = useQueryHandler();
const { parsedQuery } = getParsedQuery();
const { queryParams } = getQueryParamsWithUpdates({
updates: [{ key: 'repo', value: 'updated_mock_repo' }],
});
return (
<div data-testid={TEST_ID}>
{JSON.stringify({ parsedQuery, queryParams }, null, 2)}
</div>
);
};
describe('useQueryHandler', () => {
it('should get parsedQuery and queryParams', () => {
const { getByTestId } = render(<MockComponent />);
const smt = getByTestId(TEST_ID).innerHTML;
expect(smt).toMatchInlineSnapshot(`
"{
\\"parsedQuery\\": {
\\"versioningStrategy\\": \\"semver\\",
\\"owner\\": \\"mock_owner\\",
\\"repo\\": \\"mock_repo\\"
},
\\"queryParams\\": \\"versioningStrategy=semver&amp;owner=mock_owner&amp;repo=updated_mock_repo\\"
}"
`);
});
});
@@ -0,0 +1,68 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useLocation } from 'react-router';
import qs from 'qs';
import { Project } from '../contexts/ProjectContext';
export function useQueryHandler() {
const location = useLocation();
function getParsedQuery() {
const { decodedSearch } = getDecodedSearch(location);
const parsedQuery: Partial<Project> = qs.parse(decodedSearch);
return {
parsedQuery,
};
}
function getQueryParamsWithUpdates({
updates,
}: {
updates: {
key: keyof Project;
value: string;
}[];
}) {
const { decodedSearch } = getDecodedSearch(location);
const queryParams = qs.parse(decodedSearch);
for (const { key, value } of updates) {
queryParams[key] = value;
}
return {
queryParams: qs.stringify(queryParams),
};
}
return {
getParsedQuery,
getQueryParamsWithUpdates,
};
}
function getDecodedSearch(location: ReturnType<typeof useLocation>) {
return {
decodedSearch: new URLSearchParams(location.search).toString(),
};
}
export const testables = {
getDecodedSearch,
};
@@ -24,15 +24,18 @@ import {
IPluginApiClient,
} from '../api/PluginApiClient';
const mockOwner = 'mock_owner';
const mockRepo = 'mock_repo';
export const mockSemverProject: Project = {
owner: 'mock_owner',
repo: 'mock_repo',
owner: mockOwner,
repo: mockRepo,
versioningStrategy: 'semver',
};
export const mockCalverProject: Project = {
owner: 'mock_owner',
repo: 'mock_repo',
owner: mockOwner,
repo: mockRepo,
versioningStrategy: 'calver',
};
@@ -130,18 +133,18 @@ export const mockSelectedPatchCommit = createMockRecentCommit({
export const mockApiClient: IPluginApiClient = {
getHost: jest.fn(() => 'github.com'),
getRepoPath: jest.fn(() => 'erikengervall/playground'),
getRepoPath: jest.fn(() => `${mockOwner}/${mockRepo}`),
getOwners: jest.fn(async () => ({
owners: ['owner1', 'owner2'],
owners: [mockOwner, `${mockOwner}2`],
})),
getRepositories: jest.fn(async () => ({
repositories: ['repo1', 'repo2'],
repositories: [mockRepo, `${mockRepo}2`],
})),
getUsername: jest.fn(async () => ({
username: 'erikengervall',
username: mockOwner,
})),
getRecentCommits: jest.fn(async () => [