github-issue-tower-defence-management 1.112.6 → 1.114.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/bin/adapter/entry-points/cli/index.js +1 -0
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/consoleReadApi.js +58 -2
  6. package/bin/adapter/entry-points/console/consoleReadApi.js.map +1 -1
  7. package/bin/adapter/entry-points/console/ui-dist/assets/{index-DKFbU8lr.css → index-Blb_xqIx.css} +1 -1
  8. package/bin/adapter/entry-points/console/ui-dist/assets/index-DQyZ8KpG.js +101 -0
  9. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  10. package/bin/adapter/entry-points/console/webServer.js +6 -0
  11. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  12. package/package.json +1 -1
  13. package/src/adapter/entry-points/cli/index.ts +5 -1
  14. package/src/adapter/entry-points/console/consoleReadApi.test.ts +171 -0
  15. package/src/adapter/entry-points/console/consoleReadApi.ts +92 -1
  16. package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +44 -0
  17. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +31 -8
  18. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.stories.tsx +16 -0
  19. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.test.tsx +61 -0
  20. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.tsx +59 -5
  21. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleReferenceLink.stories.tsx +76 -0
  22. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleReferenceLink.test.tsx +56 -0
  23. package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleReferenceLink.tsx +39 -0
  24. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCommentList.tsx +4 -0
  25. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.stories.tsx +64 -3
  26. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +68 -2
  27. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +21 -0
  28. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.test.tsx +46 -0
  29. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +12 -0
  30. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestStatusBadges.stories.tsx +41 -0
  31. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestStatusBadges.test.tsx +72 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestStatusBadges.tsx +71 -0
  33. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +5 -0
  34. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.test.ts +21 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.ts +18 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleReferenceLink.test.ts +48 -0
  37. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleReferenceLink.ts +39 -0
  38. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +66 -2
  39. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +38 -6
  40. package/src/adapter/entry-points/console/ui/src/features/console/logic/references.test.ts +80 -0
  41. package/src/adapter/entry-points/console/ui/src/features/console/logic/references.ts +44 -0
  42. package/src/adapter/entry-points/console/ui/src/features/console/logic/types.ts +10 -0
  43. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +10 -0
  44. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +13 -0
  45. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleReferenceLinkContainer.tsx +25 -0
  46. package/src/adapter/entry-points/console/ui/src/features/console/testing/fixtures.ts +40 -0
  47. package/src/adapter/entry-points/console/ui/src/index.css +35 -0
  48. package/src/adapter/entry-points/console/ui-dist/assets/{index-DKFbU8lr.css → index-Blb_xqIx.css} +1 -1
  49. package/src/adapter/entry-points/console/ui-dist/assets/index-DQyZ8KpG.js +101 -0
  50. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  51. package/src/adapter/entry-points/console/webServer.test.ts +52 -1
  52. package/src/adapter/entry-points/console/webServer.ts +13 -0
  53. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  54. package/types/adapter/entry-points/console/consoleReadApi.d.ts +21 -0
  55. package/types/adapter/entry-points/console/consoleReadApi.d.ts.map +1 -1
  56. package/types/adapter/entry-points/console/webServer.d.ts +2 -1
  57. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  58. package/bin/adapter/entry-points/console/ui-dist/assets/index-DUygHx5u.js +0 -101
  59. package/src/adapter/entry-points/console/ui-dist/assets/index-DUygHx5u.js +0 -101
@@ -38,6 +38,52 @@ describe('ConsolePullRequestDetail', () => {
38
38
  expect(getByText('27 files')).toBeInTheDocument();
39
39
  });
40
40
 
41
+ it('renders the CI status badge derived from the pull request fields', () => {
42
+ const { getByText } = render(
43
+ <ConsolePullRequestDetail
44
+ pullRequest={pullRequest}
45
+ body={pullRequest.summary?.body ?? ''}
46
+ bodyIsLoading={false}
47
+ files={consoleChangedFilesFixture}
48
+ filesAreLoading={false}
49
+ filesError={null}
50
+ commits={consoleCommitsFixture}
51
+ commitsAreLoading={false}
52
+ commitsError={null}
53
+ now={now}
54
+ />,
55
+ );
56
+ expect(getByText('CI passing')).toBeInTheDocument();
57
+ });
58
+
59
+ it('renders failing CI, conflict and out-of-date badges for an unhealthy pull request', () => {
60
+ const { getByText } = render(
61
+ <ConsolePullRequestDetail
62
+ pullRequest={{
63
+ ...pullRequest,
64
+ isConflicted: true,
65
+ isPassedAllCiJob: false,
66
+ isCiStateSuccess: false,
67
+ isBranchOutOfDate: true,
68
+ missingRequiredCheckNames: ['build'],
69
+ }}
70
+ body={pullRequest.summary?.body ?? ''}
71
+ bodyIsLoading={false}
72
+ files={consoleChangedFilesFixture}
73
+ filesAreLoading={false}
74
+ filesError={null}
75
+ commits={consoleCommitsFixture}
76
+ commitsAreLoading={false}
77
+ commitsError={null}
78
+ now={now}
79
+ />,
80
+ );
81
+ expect(getByText('CI failing')).toBeInTheDocument();
82
+ expect(getByText(/missing: build/)).toBeInTheDocument();
83
+ expect(getByText('Conflict')).toBeInTheDocument();
84
+ expect(getByText('Out of date')).toBeInTheDocument();
85
+ });
86
+
41
87
  it('renders a copy URL button for the pull request url', () => {
42
88
  const { getByRole } = render(
43
89
  <ConsolePullRequestDetail
@@ -4,12 +4,14 @@ import type {
4
4
  ConsoleCommit,
5
5
  ConsoleRelatedPullRequest,
6
6
  } from '../../logic/types';
7
+ import type { ConsoleReferenceLinkRenderer } from '../content/ConsoleMarkdownContent';
7
8
  import { ConsoleMarkdownContent } from '../content/ConsoleMarkdownContent';
8
9
  import { ConsolePanel } from '../layout/ConsolePanel';
9
10
  import { ConsoleChangedFileList } from './ConsoleChangedFileList';
10
11
  import { ConsoleCommitList } from './ConsoleCommitList';
11
12
  import { ConsoleCopyUrlButton } from './ConsoleCopyUrlButton';
12
13
  import type { ConsoleAddInlineComment } from './ConsoleFileDiff';
14
+ import { ConsolePullRequestStatusBadges } from './ConsolePullRequestStatusBadges';
13
15
 
14
16
  export type ConsolePullRequestSectionProps = {
15
17
  pullRequest: ConsoleRelatedPullRequest;
@@ -23,6 +25,7 @@ export type ConsolePullRequestSectionProps = {
23
25
  commitsError: string | null;
24
26
  now: number;
25
27
  buildImageProxyUrl?: ImageProxyUrlBuilder;
28
+ renderReferenceLink?: ConsoleReferenceLinkRenderer;
26
29
  onAddInlineComment?: ConsoleAddInlineComment;
27
30
  };
28
31
 
@@ -38,6 +41,7 @@ export const ConsolePullRequestDetail = ({
38
41
  commitsError,
39
42
  now,
40
43
  buildImageProxyUrl,
44
+ renderReferenceLink,
41
45
  onAddInlineComment,
42
46
  }: ConsolePullRequestSectionProps) => {
43
47
  const summary = pullRequest.summary;
@@ -59,6 +63,13 @@ export const ConsolePullRequestDetail = ({
59
63
  {pullRequest.isDraft && (
60
64
  <span className="console-pr-section-state">draft</span>
61
65
  )}
66
+ <ConsolePullRequestStatusBadges
67
+ isConflicted={pullRequest.isConflicted}
68
+ isPassedAllCiJob={pullRequest.isPassedAllCiJob}
69
+ isCiStateSuccess={pullRequest.isCiStateSuccess}
70
+ isBranchOutOfDate={pullRequest.isBranchOutOfDate}
71
+ missingRequiredCheckNames={pullRequest.missingRequiredCheckNames}
72
+ />
62
73
  <ConsoleCopyUrlButton url={pullRequest.url} label="Copy PR URL" />
63
74
  <div className="console-pr-statbar">
64
75
  {pullRequest.branchName !== null && (
@@ -82,6 +93,7 @@ export const ConsolePullRequestDetail = ({
82
93
  <ConsoleMarkdownContent
83
94
  body={summary?.body ?? body}
84
95
  buildImageProxyUrl={buildImageProxyUrl}
96
+ renderReferenceLink={renderReferenceLink}
85
97
  />
86
98
  )}
87
99
  </ConsolePanel>
@@ -0,0 +1,41 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { ConsolePullRequestStatusBadges } from './ConsolePullRequestStatusBadges';
3
+
4
+ const meta: Meta<typeof ConsolePullRequestStatusBadges> = {
5
+ title: 'Console/ConsolePullRequestStatusBadges',
6
+ component: ConsolePullRequestStatusBadges,
7
+ };
8
+
9
+ export default meta;
10
+
11
+ type Story = StoryObj<typeof ConsolePullRequestStatusBadges>;
12
+
13
+ export const Passing: Story = {
14
+ args: {
15
+ isConflicted: false,
16
+ isPassedAllCiJob: true,
17
+ isCiStateSuccess: true,
18
+ isBranchOutOfDate: false,
19
+ missingRequiredCheckNames: [],
20
+ },
21
+ };
22
+
23
+ export const FailingWithMissingChecks: Story = {
24
+ args: {
25
+ isConflicted: false,
26
+ isPassedAllCiJob: false,
27
+ isCiStateSuccess: false,
28
+ isBranchOutOfDate: false,
29
+ missingRequiredCheckNames: ['build', 'test'],
30
+ },
31
+ };
32
+
33
+ export const ConflictedAndOutOfDate: Story = {
34
+ args: {
35
+ isConflicted: true,
36
+ isPassedAllCiJob: false,
37
+ isCiStateSuccess: false,
38
+ isBranchOutOfDate: true,
39
+ missingRequiredCheckNames: ['build', 'test'],
40
+ },
41
+ };
@@ -0,0 +1,72 @@
1
+ import { render } from '@testing-library/react';
2
+ import { ConsolePullRequestStatusBadges } from './ConsolePullRequestStatusBadges';
3
+
4
+ describe('ConsolePullRequestStatusBadges', () => {
5
+ it('renders a passing CI badge when all CI jobs passed and the CI state is success', () => {
6
+ const { getByText, queryByText } = render(
7
+ <ConsolePullRequestStatusBadges
8
+ isConflicted={false}
9
+ isPassedAllCiJob={true}
10
+ isCiStateSuccess={true}
11
+ isBranchOutOfDate={false}
12
+ missingRequiredCheckNames={[]}
13
+ />,
14
+ );
15
+ expect(getByText('CI passing')).toBeInTheDocument();
16
+ expect(queryByText('Conflict')).toBeNull();
17
+ expect(queryByText('Out of date')).toBeNull();
18
+ });
19
+
20
+ it('renders a failing CI badge with missing required checks', () => {
21
+ const { getByText } = render(
22
+ <ConsolePullRequestStatusBadges
23
+ isConflicted={false}
24
+ isPassedAllCiJob={false}
25
+ isCiStateSuccess={true}
26
+ isBranchOutOfDate={false}
27
+ missingRequiredCheckNames={['build', 'test']}
28
+ />,
29
+ );
30
+ expect(getByText('CI failing')).toBeInTheDocument();
31
+ expect(getByText(/missing: build, test/)).toBeInTheDocument();
32
+ });
33
+
34
+ it('renders the conflict badge when the pull request is conflicted', () => {
35
+ const { getByText } = render(
36
+ <ConsolePullRequestStatusBadges
37
+ isConflicted={true}
38
+ isPassedAllCiJob={true}
39
+ isCiStateSuccess={true}
40
+ isBranchOutOfDate={false}
41
+ missingRequiredCheckNames={[]}
42
+ />,
43
+ );
44
+ expect(getByText('Conflict')).toBeInTheDocument();
45
+ });
46
+
47
+ it('renders the out-of-date badge when the branch is behind the base branch', () => {
48
+ const { getByText } = render(
49
+ <ConsolePullRequestStatusBadges
50
+ isConflicted={false}
51
+ isPassedAllCiJob={true}
52
+ isCiStateSuccess={true}
53
+ isBranchOutOfDate={true}
54
+ missingRequiredCheckNames={[]}
55
+ />,
56
+ );
57
+ expect(getByText('Out of date')).toBeInTheDocument();
58
+ });
59
+
60
+ it('treats a successful CI state but unfinished jobs as failing', () => {
61
+ const { getByText } = render(
62
+ <ConsolePullRequestStatusBadges
63
+ isConflicted={false}
64
+ isPassedAllCiJob={false}
65
+ isCiStateSuccess={true}
66
+ isBranchOutOfDate={false}
67
+ missingRequiredCheckNames={[]}
68
+ />,
69
+ );
70
+ expect(getByText('CI failing')).toBeInTheDocument();
71
+ });
72
+ });
@@ -0,0 +1,71 @@
1
+ import { colorFromEnum } from '../../logic/colors';
2
+ import type { ConsoleColor } from '../../logic/types';
3
+
4
+ export type ConsolePullRequestStatusBadgesProps = {
5
+ isConflicted: boolean;
6
+ isPassedAllCiJob: boolean;
7
+ isCiStateSuccess: boolean;
8
+ isBranchOutOfDate: boolean;
9
+ missingRequiredCheckNames: string[];
10
+ };
11
+
12
+ const badgeStyle = (color: ConsoleColor) => {
13
+ const palette = colorFromEnum(color);
14
+ return {
15
+ color: palette.fg,
16
+ borderColor: palette.border,
17
+ backgroundColor: palette.bg,
18
+ };
19
+ };
20
+
21
+ export const ConsolePullRequestStatusBadges = ({
22
+ isConflicted,
23
+ isPassedAllCiJob,
24
+ isCiStateSuccess,
25
+ isBranchOutOfDate,
26
+ missingRequiredCheckNames,
27
+ }: ConsolePullRequestStatusBadgesProps) => {
28
+ const ciPassing = isPassedAllCiJob && isCiStateSuccess;
29
+ const ciColor: ConsoleColor = ciPassing ? 'GREEN' : 'RED';
30
+ const ciLabel = ciPassing ? 'CI passing' : 'CI failing';
31
+ const missingChecksLabel =
32
+ !ciPassing && missingRequiredCheckNames.length > 0
33
+ ? `missing: ${missingRequiredCheckNames.join(', ')}`
34
+ : null;
35
+
36
+ return (
37
+ <span className="console-detail-pr-status">
38
+ <span
39
+ className="console-detail-status-chip console-detail-ci-chip"
40
+ style={badgeStyle(ciColor)}
41
+ title={missingChecksLabel ?? ciLabel}
42
+ >
43
+ {ciLabel}
44
+ {missingChecksLabel !== null && (
45
+ <span className="console-detail-ci-missing">
46
+ {' '}
47
+ ({missingChecksLabel})
48
+ </span>
49
+ )}
50
+ </span>
51
+ {isConflicted && (
52
+ <span
53
+ className="console-detail-status-chip console-detail-conflict-chip"
54
+ style={badgeStyle('RED')}
55
+ title="This pull request has merge conflicts"
56
+ >
57
+ Conflict
58
+ </span>
59
+ )}
60
+ {isBranchOutOfDate && (
61
+ <span
62
+ className="console-detail-status-chip console-detail-outofdate-chip"
63
+ style={badgeStyle('YELLOW')}
64
+ title="This branch is out of date with the base branch"
65
+ >
66
+ Out of date
67
+ </span>
68
+ )}
69
+ </span>
70
+ );
71
+ };
@@ -9,6 +9,7 @@ import type {
9
9
  ConsoleComment,
10
10
  ConsoleCommit,
11
11
  ConsoleIssueState,
12
+ ConsolePullRequestStatus,
12
13
  ConsoleRelatedPullRequest,
13
14
  } from '../logic/types';
14
15
  import { useConsoleToken } from './useConsoleToken';
@@ -20,6 +21,7 @@ export type ConsoleCaches = {
20
21
  commits: ResourceCache<ConsoleCommit[]>;
21
22
  relatedPrs: ResourceCache<ConsoleRelatedPullRequest[]>;
22
23
  state: ResourceCache<ConsoleIssueState>;
24
+ prStatus: ResourceCache<ConsolePullRequestStatus>;
23
25
  client: ConsoleApiClient;
24
26
  };
25
27
 
@@ -37,6 +39,9 @@ export const useConsoleCaches = (): ConsoleCaches => {
37
39
  client.fetchRelatedPrs,
38
40
  ),
39
41
  state: new ResourceCache<ConsoleIssueState>(client.fetchIssueState),
42
+ prStatus: new ResourceCache<ConsolePullRequestStatus>(
43
+ client.fetchPullRequestStatus,
44
+ ),
40
45
  };
41
46
  }, [appendToken]);
42
47
  };
@@ -6,6 +6,7 @@ import type {
6
6
  ConsoleCommit,
7
7
  ConsoleIssueState,
8
8
  ConsoleListItem,
9
+ ConsolePullRequestStatus,
9
10
  ConsoleRelatedPullRequest,
10
11
  } from '../logic/types';
11
12
  import type { ConsoleCaches } from './useConsoleCaches';
@@ -62,6 +63,15 @@ const buildCaches = (related: ConsoleRelatedPullRequest[]): ConsoleCaches => {
62
63
  state: 'open',
63
64
  merged: false,
64
65
  isPullRequest: true,
66
+ title: 'Console item detail fixture title',
67
+ }),
68
+ fetchPullRequestStatus: async (): Promise<ConsolePullRequestStatus> => ({
69
+ found: true,
70
+ isConflicted: true,
71
+ isPassedAllCiJob: false,
72
+ isCiStateSuccess: false,
73
+ isBranchOutOfDate: true,
74
+ missingRequiredCheckNames: ['build'],
65
75
  }),
66
76
  };
67
77
  return {
@@ -72,6 +82,7 @@ const buildCaches = (related: ConsoleRelatedPullRequest[]): ConsoleCaches => {
72
82
  commits: new ResourceCache(client.fetchPrCommits),
73
83
  relatedPrs: new ResourceCache(client.fetchRelatedPrs),
74
84
  state: new ResourceCache(client.fetchIssueState),
85
+ prStatus: new ResourceCache(client.fetchPullRequestStatus),
75
86
  };
76
87
  };
77
88
 
@@ -85,9 +96,19 @@ describe('useConsoleItemDetailData', () => {
85
96
  expect(result.current.body).toBe('body');
86
97
  expect(result.current.files.length).toBe(1);
87
98
  expect(result.current.commits.length).toBe(1);
99
+ expect(result.current.pullRequestStatus?.found).toBe(true);
100
+ expect(result.current.pullRequestStatus?.isConflicted).toBe(true);
88
101
  });
89
102
  });
90
103
 
104
+ it('does not expose pull request status for an issue item', () => {
105
+ const caches = buildCaches([]);
106
+ const { result } = renderHook(() =>
107
+ useConsoleItemDetailData(caches, issueItem),
108
+ );
109
+ expect(result.current.pullRequestStatus).toBeNull();
110
+ });
111
+
91
112
  it('loads related pull request views for an issue item', async () => {
92
113
  const related: ConsoleRelatedPullRequest[] = [
93
114
  {
@@ -6,6 +6,7 @@ import type {
6
6
  ConsoleCommit,
7
7
  ConsoleIssueState,
8
8
  ConsoleListItem,
9
+ ConsolePullRequestStatus,
9
10
  ConsoleRelatedPullRequest,
10
11
  } from '../logic/types';
11
12
  import type { ConsoleCaches } from './useConsoleCaches';
@@ -19,6 +20,15 @@ const DEFAULT_STATE: ConsoleIssueState = {
19
20
  state: 'open',
20
21
  merged: false,
21
22
  isPullRequest: false,
23
+ title: '',
24
+ };
25
+ const DEFAULT_PR_STATUS: ConsolePullRequestStatus = {
26
+ found: false,
27
+ isConflicted: false,
28
+ isPassedAllCiJob: false,
29
+ isCiStateSuccess: false,
30
+ isBranchOutOfDate: false,
31
+ missingRequiredCheckNames: [],
22
32
  };
23
33
 
24
34
  export type ConsoleItemDetailData = {
@@ -35,6 +45,7 @@ export type ConsoleItemDetailData = {
35
45
  commits: ConsoleCommit[];
36
46
  commitsAreLoading: boolean;
37
47
  commitsError: string | null;
48
+ pullRequestStatus: ConsolePullRequestStatus | null;
38
49
  relatedPullRequests: ConsoleRelatedPullRequestView[];
39
50
  };
40
51
 
@@ -66,6 +77,12 @@ export const useConsoleItemDetailData = (
66
77
  isPr ? url : null,
67
78
  EMPTY_COMMITS,
68
79
  );
80
+ const prStatus = useConsoleResource(
81
+ caches.prStatus,
82
+ isPr ? key : null,
83
+ isPr ? url : null,
84
+ DEFAULT_PR_STATUS,
85
+ );
69
86
  const relatedPrs = useConsoleResource(
70
87
  caches.relatedPrs,
71
88
  !isPr ? key : null,
@@ -162,6 +179,7 @@ export const useConsoleItemDetailData = (
162
179
  commits: commits.data,
163
180
  commitsAreLoading: commits.isLoading,
164
181
  commitsError: commits.error,
182
+ pullRequestStatus: isPr ? prStatus.data : null,
165
183
  relatedPullRequests: relatedViews,
166
184
  };
167
185
  };
@@ -0,0 +1,48 @@
1
+ import { renderHook, waitFor } from '@testing-library/react';
2
+ import { ResourceCache } from '../lib/resourceCache';
3
+ import type { ConsoleIssueState } from '../logic/types';
4
+ import { useConsoleReferenceLink } from './useConsoleReferenceLink';
5
+
6
+ const buildState = (title: string): ConsoleIssueState => ({
7
+ state: 'open',
8
+ merged: false,
9
+ isPullRequest: true,
10
+ title,
11
+ });
12
+
13
+ describe('useConsoleReferenceLink', () => {
14
+ const url = 'https://github.com/octo/repo/pull/7';
15
+
16
+ it('returns null then the resolved state', async () => {
17
+ const cache = new ResourceCache<ConsoleIssueState>(async () =>
18
+ buildState('Decorate links'),
19
+ );
20
+ const { result } = renderHook(() => useConsoleReferenceLink(cache, url));
21
+ expect(result.current).toBeNull();
22
+ await waitFor(() => {
23
+ expect(result.current?.title).toBe('Decorate links');
24
+ });
25
+ });
26
+
27
+ it('returns null when the fetch fails', async () => {
28
+ const cache = new ResourceCache<ConsoleIssueState>(async () => {
29
+ throw new Error('boom');
30
+ });
31
+ const { result } = renderHook(() => useConsoleReferenceLink(cache, url));
32
+ await waitFor(() => {
33
+ expect(cache.has(url)).toBe(false);
34
+ });
35
+ expect(result.current).toBeNull();
36
+ });
37
+
38
+ it('reuses an already cached value without refetching', () => {
39
+ const fetcher = jest.fn(async () => buildState('cached'));
40
+ const cache = new ResourceCache<ConsoleIssueState>(fetcher);
41
+ cache.load(url, url);
42
+ return cache.load(url, url).then(() => {
43
+ const { result } = renderHook(() => useConsoleReferenceLink(cache, url));
44
+ expect(result.current?.title).toBe('cached');
45
+ expect(fetcher).toHaveBeenCalledTimes(1);
46
+ });
47
+ });
48
+ });
@@ -0,0 +1,39 @@
1
+ import { useEffect, useState } from 'react';
2
+ import type { ResourceCache } from '../lib/resourceCache';
3
+ import type { ConsoleIssueState } from '../logic/types';
4
+
5
+ export const useConsoleReferenceLink = (
6
+ cache: ResourceCache<ConsoleIssueState>,
7
+ url: string,
8
+ ): ConsoleIssueState | null => {
9
+ const [state, setState] = useState<ConsoleIssueState | null>(
10
+ () => cache.peek(url) ?? null,
11
+ );
12
+
13
+ useEffect(() => {
14
+ const cached = cache.peek(url);
15
+ if (cached !== undefined) {
16
+ setState(cached);
17
+ return;
18
+ }
19
+ let cancelled = false;
20
+ setState(null);
21
+ cache
22
+ .load(url, url)
23
+ .then((value) => {
24
+ if (!cancelled) {
25
+ setState(value);
26
+ }
27
+ })
28
+ .catch(() => {
29
+ if (!cancelled) {
30
+ setState(null);
31
+ }
32
+ });
33
+ return () => {
34
+ cancelled = true;
35
+ };
36
+ }, [cache, url]);
37
+
38
+ return state;
39
+ };
@@ -48,6 +48,7 @@ describe('createConsoleApiClient', () => {
48
48
  readers.push((url) => client.fetchPrCommits(url));
49
49
  readers.push((url) => client.fetchRelatedPrs(url));
50
50
  readers.push((url) => client.fetchIssueState(url));
51
+ readers.push((url) => client.fetchPullRequestStatus(url));
51
52
  const expectedPaths = [
52
53
  '/api/itembody',
53
54
  '/api/comments',
@@ -55,6 +56,7 @@ describe('createConsoleApiClient', () => {
55
56
  '/api/prcommits',
56
57
  '/api/relatedprs',
57
58
  '/api/issuetitle',
59
+ '/api/pullrequeststatus',
58
60
  ];
59
61
  for (let index = 0; index < readers.length; index += 1) {
60
62
  const fetchMock = mockFetchOnce({});
@@ -103,11 +105,34 @@ describe('createConsoleApiClient', () => {
103
105
  });
104
106
 
105
107
  it('parses the issue state', async () => {
106
- mockFetchOnce({ state: 'closed', merged: true, isPullRequest: true });
108
+ mockFetchOnce({
109
+ state: 'closed',
110
+ merged: true,
111
+ isPullRequest: true,
112
+ title: 'Decorate PR links',
113
+ });
107
114
  const client = createConsoleApiClient(appendToken);
108
115
  expect(
109
116
  await client.fetchIssueState('https://github.com/o/r/pull/1'),
110
- ).toEqual({ state: 'closed', merged: true, isPullRequest: true });
117
+ ).toEqual({
118
+ state: 'closed',
119
+ merged: true,
120
+ isPullRequest: true,
121
+ title: 'Decorate PR links',
122
+ });
123
+ });
124
+
125
+ it('defaults the title to an empty string when absent', async () => {
126
+ mockFetchOnce({ state: 'open', merged: false, isPullRequest: false });
127
+ const client = createConsoleApiClient(appendToken);
128
+ expect(
129
+ await client.fetchIssueState('https://github.com/o/r/issues/1'),
130
+ ).toEqual({
131
+ state: 'open',
132
+ merged: false,
133
+ isPullRequest: false,
134
+ title: '',
135
+ });
111
136
  });
112
137
 
113
138
  it('parses pull request commits', async () => {
@@ -161,6 +186,45 @@ describe('createConsoleApiClient', () => {
161
186
  expect(related[0].missingRequiredCheckNames).toEqual(['build']);
162
187
  });
163
188
 
189
+ it('parses the pull request status when found', async () => {
190
+ mockFetchOnce({
191
+ found: true,
192
+ status: {
193
+ isConflicted: true,
194
+ isPassedAllCiJob: false,
195
+ isCiStateSuccess: false,
196
+ isBranchOutOfDate: true,
197
+ missingRequiredCheckNames: ['build', 'test'],
198
+ },
199
+ });
200
+ const client = createConsoleApiClient(appendToken);
201
+ expect(
202
+ await client.fetchPullRequestStatus('https://github.com/o/r/pull/1'),
203
+ ).toEqual({
204
+ found: true,
205
+ isConflicted: true,
206
+ isPassedAllCiJob: false,
207
+ isCiStateSuccess: false,
208
+ isBranchOutOfDate: true,
209
+ missingRequiredCheckNames: ['build', 'test'],
210
+ });
211
+ });
212
+
213
+ it('returns a not-found pull request status when the server reports none', async () => {
214
+ mockFetchOnce({ found: false, status: null });
215
+ const client = createConsoleApiClient(appendToken);
216
+ expect(
217
+ await client.fetchPullRequestStatus('https://github.com/o/r/pull/1'),
218
+ ).toEqual({
219
+ found: false,
220
+ isConflicted: false,
221
+ isPassedAllCiJob: false,
222
+ isCiStateSuccess: false,
223
+ isBranchOutOfDate: false,
224
+ missingRequiredCheckNames: [],
225
+ });
226
+ });
227
+
164
228
  it('throws on a non-ok response', async () => {
165
229
  mockFetchOnce({}, false);
166
230
  const client = createConsoleApiClient(appendToken);