github-issue-tower-defence-management 1.101.6 → 1.101.8
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.
- package/CHANGELOG.md +14 -0
- package/bin/adapter/entry-points/console/consoleServer.js +32 -10
- package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-ES6SLB1Y.css +1 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-_g2CqG11.js +101 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapter/entry-points/console/consoleServer.test.ts +43 -0
- package/src/adapter/entry-points/console/consoleServer.ts +38 -11
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +139 -145
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleErrorToast.stories.tsx +23 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleUndoToast.test.tsx +44 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/operations/ConsoleUndoToast.tsx +25 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleActionQueue.test.ts +84 -1
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleActionQueue.ts +31 -6
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +2 -2
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +2 -4
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +37 -10
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +19 -1
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +8 -23
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +0 -97
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +10 -40
- package/src/adapter/entry-points/console/ui/src/index.css +6 -16
- package/src/adapter/entry-points/console/ui-dist/assets/index-ES6SLB1Y.css +1 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-_g2CqG11.js +101 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-D4zghbcI.css +0 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DX0mapkS.js +0 -101
- package/src/adapter/entry-points/console/ui-dist/assets/index-D4zghbcI.css +0 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-DX0mapkS.js +0 -101
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>TDPM Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-_g2CqG11.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-ES6SLB1Y.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -652,6 +652,49 @@ describe('consoleServer new routes integration', () => {
|
|
|
652
652
|
}
|
|
653
653
|
});
|
|
654
654
|
|
|
655
|
+
it('returns 502 with the underlying error message when an operation rejects', async () => {
|
|
656
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
657
|
+
const issueRepository = mock<IssueRepository>();
|
|
658
|
+
issueRepository.get.mockResolvedValue({
|
|
659
|
+
...mock<Issue>(),
|
|
660
|
+
itemId: 'PVTI_loaded',
|
|
661
|
+
});
|
|
662
|
+
issueRepository.approvePullRequest.mockRejectedValue(
|
|
663
|
+
new Error('Failed to approve PR https://github.com/o/r/pull/1: HTTP 422'),
|
|
664
|
+
);
|
|
665
|
+
const server = await startConsoleServer({
|
|
666
|
+
accessToken: testToken,
|
|
667
|
+
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
668
|
+
consoleDataOutputDir: null,
|
|
669
|
+
inTmuxDataDir: null,
|
|
670
|
+
dashboardDir: null,
|
|
671
|
+
issueRepository,
|
|
672
|
+
resolveProject: async (pjcode) =>
|
|
673
|
+
pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
|
|
674
|
+
port: 0,
|
|
675
|
+
});
|
|
676
|
+
try {
|
|
677
|
+
const response = await request(
|
|
678
|
+
server,
|
|
679
|
+
'POST',
|
|
680
|
+
`/api/review?k=${testToken}`,
|
|
681
|
+
{
|
|
682
|
+
pjcode: 'umino',
|
|
683
|
+
action: 'approve',
|
|
684
|
+
prUrl: 'https://github.com/o/r/pull/1',
|
|
685
|
+
projectItemId: 'PVTI_op',
|
|
686
|
+
},
|
|
687
|
+
);
|
|
688
|
+
expect(response.statusCode).toBe(502);
|
|
689
|
+
expect(JSON.parse(response.body)).toEqual({
|
|
690
|
+
error: 'Failed to approve PR https://github.com/o/r/pull/1: HTTP 422',
|
|
691
|
+
});
|
|
692
|
+
} finally {
|
|
693
|
+
await closeServer(server);
|
|
694
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
|
|
655
698
|
it('rejects an operation api with a malformed json body', async () => {
|
|
656
699
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
657
700
|
const issueRepository = mock<IssueRepository>();
|
|
@@ -372,6 +372,32 @@ const handleReadApi = async (
|
|
|
372
372
|
}
|
|
373
373
|
};
|
|
374
374
|
|
|
375
|
+
const operationErrorMessage = (error: unknown): string => {
|
|
376
|
+
if (error instanceof Error && error.message.length > 0) {
|
|
377
|
+
return error.message;
|
|
378
|
+
}
|
|
379
|
+
return String(error);
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const dispatchOperation = (
|
|
383
|
+
context: ConsoleOperationContext,
|
|
384
|
+
requestPath: string,
|
|
385
|
+
body: Record<string, unknown>,
|
|
386
|
+
): Promise<{ statusCode: number; body: unknown }> | null => {
|
|
387
|
+
switch (requestPath) {
|
|
388
|
+
case '/api/review':
|
|
389
|
+
return handleReview(context, body);
|
|
390
|
+
case '/api/triage':
|
|
391
|
+
return handleTriage(context, body);
|
|
392
|
+
case '/api/intmux':
|
|
393
|
+
return handleIntmux(context, body);
|
|
394
|
+
case '/api/comment':
|
|
395
|
+
return handleComment(context, body);
|
|
396
|
+
default:
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
375
401
|
const handleOperationApi = async (
|
|
376
402
|
options: ConsoleServerOptions,
|
|
377
403
|
requestPath: string,
|
|
@@ -387,17 +413,18 @@ const handleOperationApi = async (
|
|
|
387
413
|
resolveProject,
|
|
388
414
|
consoleDataOutputDir: options.consoleDataOutputDir,
|
|
389
415
|
};
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
416
|
+
const dispatched = dispatchOperation(context, requestPath, body);
|
|
417
|
+
if (dispatched === null) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
return await dispatched;
|
|
422
|
+
} catch (error) {
|
|
423
|
+
console.error('console operation failed', error);
|
|
424
|
+
return {
|
|
425
|
+
statusCode: 502,
|
|
426
|
+
body: { error: operationErrorMessage(error) },
|
|
427
|
+
};
|
|
401
428
|
}
|
|
402
429
|
};
|
|
403
430
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReactNode
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
2
|
import type { ImageProxyUrlBuilder } from '../../lib/imageProxy';
|
|
3
3
|
import { colorFromEnum } from '../../logic/colors';
|
|
4
4
|
import {
|
|
@@ -55,7 +55,6 @@ export type ConsoleItemDetailProps = {
|
|
|
55
55
|
now: number;
|
|
56
56
|
commentComposer: ReactNode;
|
|
57
57
|
operationBar: ReactNode;
|
|
58
|
-
scrollRef?: Ref<HTMLElement>;
|
|
59
58
|
buildImageProxyUrl?: ImageProxyUrlBuilder;
|
|
60
59
|
};
|
|
61
60
|
|
|
@@ -81,7 +80,6 @@ export const ConsoleItemDetail = ({
|
|
|
81
80
|
now,
|
|
82
81
|
commentComposer,
|
|
83
82
|
operationBar,
|
|
84
|
-
scrollRef,
|
|
85
83
|
buildImageProxyUrl,
|
|
86
84
|
}: ConsoleItemDetailProps) => {
|
|
87
85
|
const resolvedState = state?.state ?? 'open';
|
|
@@ -100,170 +98,166 @@ export const ConsoleItemDetail = ({
|
|
|
100
98
|
commitsAreLoading || commitsError !== null ? null : commits.length;
|
|
101
99
|
|
|
102
100
|
return (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<
|
|
107
|
-
<span
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)}
|
|
101
|
+
<article className="console-detail">
|
|
102
|
+
{storyName !== null && (
|
|
103
|
+
<div className="console-detail-story">
|
|
104
|
+
<span className="console-storytag">
|
|
105
|
+
<span
|
|
106
|
+
className="console-story-dot"
|
|
107
|
+
style={{ backgroundColor: storyPalette.dot }}
|
|
108
|
+
/>
|
|
109
|
+
{storyName}
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
116
113
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
114
|
+
{overlayStatus !== null && statusPalette !== null && (
|
|
115
|
+
<span
|
|
116
|
+
className="console-detail-status-chip"
|
|
117
|
+
style={{
|
|
118
|
+
color: statusPalette.fg,
|
|
119
|
+
borderColor: statusPalette.border,
|
|
120
|
+
backgroundColor: statusPalette.bg,
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{overlayStatus.name}
|
|
124
|
+
</span>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
<h2 className="console-detail-title">
|
|
128
|
+
<ConsoleItemIcon
|
|
129
|
+
isPr={item.isPr}
|
|
130
|
+
state={resolvedState}
|
|
131
|
+
merged={merged}
|
|
132
|
+
isDraft={false}
|
|
133
|
+
stateReason=""
|
|
134
|
+
/>
|
|
135
|
+
<span className="console-detail-title-text">{item.title}</span>
|
|
136
|
+
<span className="console-detail-number">
|
|
137
|
+
{item.isPr ? `PR #${item.number}` : `#${item.number}`}
|
|
138
|
+
</span>
|
|
139
|
+
{closedStateLabel !== null && (
|
|
140
|
+
<span className="console-detail-closed-label">
|
|
141
|
+
{closedStateLabel}
|
|
127
142
|
</span>
|
|
128
143
|
)}
|
|
144
|
+
</h2>
|
|
129
145
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
<div className="console-detail-subbar">
|
|
147
|
+
<a
|
|
148
|
+
href={item.url}
|
|
149
|
+
className="console-detail-link"
|
|
150
|
+
target="_blank"
|
|
151
|
+
rel="noopener noreferrer"
|
|
152
|
+
>
|
|
153
|
+
{item.isPr ? `PR #${item.number}` : `Issue #${item.number}`}
|
|
154
|
+
</a>
|
|
155
|
+
<span className="console-detail-repo">{item.repo}</span>
|
|
156
|
+
<span className="console-detail-pill">
|
|
157
|
+
{item.isPr ? 'PR' : 'Issue'}
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{item.labels.length > 0 && (
|
|
162
|
+
<div className="console-detail-labels">
|
|
163
|
+
{item.labels.map((label) => (
|
|
164
|
+
<span key={label} className="console-label-chip">
|
|
165
|
+
{label}
|
|
145
166
|
</span>
|
|
146
|
-
)}
|
|
147
|
-
</
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
<div
|
|
172
|
+
className="console-detail-createdat"
|
|
173
|
+
title={formatFullTimestamp(item.createdAt)}
|
|
174
|
+
>
|
|
175
|
+
opened {formatRelativeTime(item.createdAt, now)}
|
|
176
|
+
</div>
|
|
148
177
|
|
|
149
|
-
|
|
178
|
+
<ConsolePanel
|
|
179
|
+
title="Description"
|
|
180
|
+
headerAction={
|
|
150
181
|
<a
|
|
151
182
|
href={item.url}
|
|
152
|
-
className="console-
|
|
183
|
+
className="console-panel-open-link"
|
|
153
184
|
target="_blank"
|
|
154
185
|
rel="noopener noreferrer"
|
|
155
186
|
>
|
|
156
|
-
|
|
187
|
+
open
|
|
157
188
|
</a>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
</div>
|
|
189
|
+
}
|
|
190
|
+
>
|
|
191
|
+
{bodyError !== null ? (
|
|
192
|
+
<p role="alert" className="console-detail-body-error">
|
|
193
|
+
Failed to load description: {bodyError}
|
|
194
|
+
</p>
|
|
195
|
+
) : bodyIsLoading ? (
|
|
196
|
+
<p className="console-detail-body-loading">Loading description...</p>
|
|
197
|
+
) : (
|
|
198
|
+
<ConsoleMarkdownContent
|
|
199
|
+
body={body}
|
|
200
|
+
buildImageProxyUrl={buildImageProxyUrl}
|
|
201
|
+
/>
|
|
172
202
|
)}
|
|
203
|
+
</ConsolePanel>
|
|
173
204
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
<ConsolePanel
|
|
182
|
-
title="Description"
|
|
183
|
-
headerAction={
|
|
184
|
-
<a
|
|
185
|
-
href={item.url}
|
|
186
|
-
className="console-panel-open-link"
|
|
187
|
-
target="_blank"
|
|
188
|
-
rel="noopener noreferrer"
|
|
189
|
-
>
|
|
190
|
-
open
|
|
191
|
-
</a>
|
|
192
|
-
}
|
|
193
|
-
>
|
|
194
|
-
{bodyError !== null ? (
|
|
195
|
-
<p role="alert" className="console-detail-body-error">
|
|
196
|
-
Failed to load description: {bodyError}
|
|
197
|
-
</p>
|
|
198
|
-
) : bodyIsLoading ? (
|
|
199
|
-
<p className="console-detail-body-loading">
|
|
200
|
-
Loading description...
|
|
201
|
-
</p>
|
|
202
|
-
) : (
|
|
203
|
-
<ConsoleMarkdownContent
|
|
204
|
-
body={body}
|
|
205
|
-
buildImageProxyUrl={buildImageProxyUrl}
|
|
206
|
-
/>
|
|
207
|
-
)}
|
|
205
|
+
{item.isPr && (
|
|
206
|
+
<ConsolePanel title="Changed files" count={filesCount}>
|
|
207
|
+
<ConsoleChangedFileList
|
|
208
|
+
files={files}
|
|
209
|
+
isLoading={filesAreLoading}
|
|
210
|
+
error={filesError}
|
|
211
|
+
/>
|
|
208
212
|
</ConsolePanel>
|
|
213
|
+
)}
|
|
209
214
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
<ConsolePanel
|
|
216
|
+
title="Comments"
|
|
217
|
+
count={commentsCount}
|
|
218
|
+
defaultCollapsed={item.isPr}
|
|
219
|
+
>
|
|
220
|
+
<ConsoleCommentList
|
|
221
|
+
comments={comments}
|
|
222
|
+
isLoading={commentsAreLoading}
|
|
223
|
+
error={commentsError}
|
|
224
|
+
now={now}
|
|
225
|
+
buildImageProxyUrl={buildImageProxyUrl}
|
|
226
|
+
/>
|
|
227
|
+
</ConsolePanel>
|
|
219
228
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
comments={comments}
|
|
227
|
-
isLoading={commentsAreLoading}
|
|
228
|
-
error={commentsError}
|
|
229
|
+
{item.isPr && (
|
|
230
|
+
<ConsolePanel title="Commits" count={commitsCount} defaultCollapsed>
|
|
231
|
+
<ConsoleCommitList
|
|
232
|
+
commits={commits}
|
|
233
|
+
isLoading={commitsAreLoading}
|
|
234
|
+
error={commitsError}
|
|
229
235
|
now={now}
|
|
230
|
-
buildImageProxyUrl={buildImageProxyUrl}
|
|
231
236
|
/>
|
|
232
237
|
</ConsolePanel>
|
|
238
|
+
)}
|
|
233
239
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
bodyIsLoading={false}
|
|
252
|
-
files={related.files}
|
|
253
|
-
filesAreLoading={related.filesAreLoading}
|
|
254
|
-
filesError={related.filesError}
|
|
255
|
-
commits={related.commits}
|
|
256
|
-
commitsAreLoading={related.commitsAreLoading}
|
|
257
|
-
commitsError={related.commitsError}
|
|
258
|
-
now={now}
|
|
259
|
-
buildImageProxyUrl={buildImageProxyUrl}
|
|
260
|
-
/>
|
|
261
|
-
))}
|
|
240
|
+
{!item.isPr &&
|
|
241
|
+
relatedPullRequests.map((related) => (
|
|
242
|
+
<ConsolePullRequestDetail
|
|
243
|
+
key={related.pullRequest.url}
|
|
244
|
+
pullRequest={related.pullRequest}
|
|
245
|
+
body={related.pullRequest.summary?.body ?? ''}
|
|
246
|
+
bodyIsLoading={false}
|
|
247
|
+
files={related.files}
|
|
248
|
+
filesAreLoading={related.filesAreLoading}
|
|
249
|
+
filesError={related.filesError}
|
|
250
|
+
commits={related.commits}
|
|
251
|
+
commitsAreLoading={related.commitsAreLoading}
|
|
252
|
+
commitsError={related.commitsError}
|
|
253
|
+
now={now}
|
|
254
|
+
buildImageProxyUrl={buildImageProxyUrl}
|
|
255
|
+
/>
|
|
256
|
+
))}
|
|
262
257
|
|
|
263
|
-
|
|
264
|
-
</article>
|
|
258
|
+
{commentComposer}
|
|
265
259
|
|
|
266
|
-
<
|
|
267
|
-
|
|
260
|
+
<div className="console-actionbar">{operationBar}</div>
|
|
261
|
+
</article>
|
|
268
262
|
);
|
|
269
263
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { ConsoleErrorToast } from './ConsoleUndoToast';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ConsoleErrorToast> = {
|
|
5
|
+
title: 'Console/ConsoleErrorToast',
|
|
6
|
+
component: ConsoleErrorToast,
|
|
7
|
+
args: {
|
|
8
|
+
message: '操作に失敗しました: HTTP 422 Review cannot be requested',
|
|
9
|
+
onDismiss: () => {},
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
|
|
15
|
+
type Story = StoryObj<typeof ConsoleErrorToast>;
|
|
16
|
+
|
|
17
|
+
export const ReviewRejectedByGitHub: Story = {};
|
|
18
|
+
|
|
19
|
+
export const NetworkFailure: Story = {
|
|
20
|
+
args: {
|
|
21
|
+
message: '操作に失敗しました: network down',
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fireEvent, render } from '@testing-library/react';
|
|
2
|
-
import { ConsoleUndoToast } from './ConsoleUndoToast';
|
|
2
|
+
import { ConsoleErrorToast, ConsoleUndoToast } from './ConsoleUndoToast';
|
|
3
3
|
|
|
4
4
|
describe('ConsoleUndoToast', () => {
|
|
5
5
|
const baseProps = {
|
|
@@ -52,3 +52,46 @@ describe('ConsoleUndoToast', () => {
|
|
|
52
52
|
expect(onUndo).toHaveBeenCalledTimes(1);
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
|
+
|
|
56
|
+
describe('ConsoleErrorToast', () => {
|
|
57
|
+
it('shows the failure message with the error modifier class and alert role', () => {
|
|
58
|
+
const { getByText, container, getByRole } = render(
|
|
59
|
+
<ConsoleErrorToast
|
|
60
|
+
message="操作に失敗しました: HTTP 422"
|
|
61
|
+
onDismiss={() => {}}
|
|
62
|
+
/>,
|
|
63
|
+
);
|
|
64
|
+
expect(getByText('操作に失敗しました: HTTP 422')).toBeInTheDocument();
|
|
65
|
+
expect(
|
|
66
|
+
container.querySelector('.console-undo-toast-error'),
|
|
67
|
+
).toBeInTheDocument();
|
|
68
|
+
expect(getByRole('alert')).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('does not render a countdown or progress bar', () => {
|
|
72
|
+
const { container } = render(
|
|
73
|
+
<ConsoleErrorToast
|
|
74
|
+
message="操作に失敗しました: boom"
|
|
75
|
+
onDismiss={() => {}}
|
|
76
|
+
/>,
|
|
77
|
+
);
|
|
78
|
+
expect(
|
|
79
|
+
container.querySelector('.console-undo-toast-countdown'),
|
|
80
|
+
).not.toBeInTheDocument();
|
|
81
|
+
expect(
|
|
82
|
+
container.querySelector('.console-undo-toast-bar'),
|
|
83
|
+
).not.toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('invokes onDismiss when the Dismiss control is clicked', () => {
|
|
87
|
+
const onDismiss = jest.fn();
|
|
88
|
+
const { getByText } = render(
|
|
89
|
+
<ConsoleErrorToast
|
|
90
|
+
message="操作に失敗しました: boom"
|
|
91
|
+
onDismiss={onDismiss}
|
|
92
|
+
/>,
|
|
93
|
+
);
|
|
94
|
+
fireEvent.click(getByText('Dismiss'));
|
|
95
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -34,3 +34,28 @@ export const ConsoleUndoToast = ({
|
|
|
34
34
|
/>
|
|
35
35
|
</div>
|
|
36
36
|
);
|
|
37
|
+
|
|
38
|
+
export type ConsoleErrorToastProps = {
|
|
39
|
+
message: string;
|
|
40
|
+
onDismiss: () => void;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const ConsoleErrorToast = ({
|
|
44
|
+
message,
|
|
45
|
+
onDismiss,
|
|
46
|
+
}: ConsoleErrorToastProps) => (
|
|
47
|
+
<div
|
|
48
|
+
className="console-undo-toast console-undo-toast-error"
|
|
49
|
+
role="alert"
|
|
50
|
+
aria-live="assertive"
|
|
51
|
+
>
|
|
52
|
+
<span className="console-undo-toast-message">{message}</span>
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
className="console-undo-toast-undo"
|
|
56
|
+
onClick={onDismiss}
|
|
57
|
+
>
|
|
58
|
+
Dismiss
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleActionQueue.test.ts
CHANGED
|
@@ -8,11 +8,14 @@ const makeAction = (
|
|
|
8
8
|
) => ({
|
|
9
9
|
message: 'Approved — PR #851',
|
|
10
10
|
color: 'green' as const,
|
|
11
|
-
commit: jest.fn(),
|
|
11
|
+
commit: jest.fn<Promise<void>, []>().mockResolvedValue(undefined),
|
|
12
12
|
advance: jest.fn(),
|
|
13
13
|
...overrides,
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
+
const flushMicrotasks = (): Promise<void> =>
|
|
17
|
+
Promise.resolve().then(() => undefined);
|
|
18
|
+
|
|
16
19
|
describe('useConsoleActionQueue', () => {
|
|
17
20
|
beforeEach(() => {
|
|
18
21
|
jest.useFakeTimers();
|
|
@@ -114,4 +117,84 @@ describe('useConsoleActionQueue', () => {
|
|
|
114
117
|
expect(first.commit).toHaveBeenCalledTimes(1);
|
|
115
118
|
expect(second.commit).toHaveBeenCalledTimes(1);
|
|
116
119
|
});
|
|
120
|
+
|
|
121
|
+
it('surfaces the failure reason when the timer commit rejects', async () => {
|
|
122
|
+
const { result } = renderHook(() => useConsoleActionQueue());
|
|
123
|
+
const action = makeAction({
|
|
124
|
+
commit: jest
|
|
125
|
+
.fn<Promise<void>, []>()
|
|
126
|
+
.mockRejectedValue(new Error('HTTP 422 review cannot be requested')),
|
|
127
|
+
});
|
|
128
|
+
act(() => {
|
|
129
|
+
result.current.enqueue(action);
|
|
130
|
+
});
|
|
131
|
+
expect(result.current.error).toBeNull();
|
|
132
|
+
await act(async () => {
|
|
133
|
+
jest.advanceTimersByTime(5000);
|
|
134
|
+
await flushMicrotasks();
|
|
135
|
+
});
|
|
136
|
+
expect(action.commit).toHaveBeenCalledTimes(1);
|
|
137
|
+
expect(result.current.error).toEqual({
|
|
138
|
+
message: 'Approved — PR #851',
|
|
139
|
+
reason: 'HTTP 422 review cannot be requested',
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('surfaces the failure reason when the previous action commit rejects on enqueue', async () => {
|
|
144
|
+
const { result } = renderHook(() => useConsoleActionQueue());
|
|
145
|
+
const first = makeAction({
|
|
146
|
+
commit: jest
|
|
147
|
+
.fn<Promise<void>, []>()
|
|
148
|
+
.mockRejectedValue(new Error('network down')),
|
|
149
|
+
});
|
|
150
|
+
const second = makeAction({
|
|
151
|
+
message: 'Rejected — PR #853',
|
|
152
|
+
color: 'amber',
|
|
153
|
+
});
|
|
154
|
+
act(() => {
|
|
155
|
+
result.current.enqueue(first);
|
|
156
|
+
});
|
|
157
|
+
await act(async () => {
|
|
158
|
+
jest.advanceTimersByTime(1000);
|
|
159
|
+
result.current.enqueue(second);
|
|
160
|
+
await flushMicrotasks();
|
|
161
|
+
});
|
|
162
|
+
expect(first.commit).toHaveBeenCalledTimes(1);
|
|
163
|
+
expect(result.current.error).toEqual({
|
|
164
|
+
message: 'Approved — PR #851',
|
|
165
|
+
reason: 'network down',
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('clears the surfaced error when dismissError is called', async () => {
|
|
170
|
+
const { result } = renderHook(() => useConsoleActionQueue());
|
|
171
|
+
const action = makeAction({
|
|
172
|
+
commit: jest.fn<Promise<void>, []>().mockRejectedValue(new Error('boom')),
|
|
173
|
+
});
|
|
174
|
+
act(() => {
|
|
175
|
+
result.current.enqueue(action);
|
|
176
|
+
});
|
|
177
|
+
await act(async () => {
|
|
178
|
+
jest.advanceTimersByTime(5000);
|
|
179
|
+
await flushMicrotasks();
|
|
180
|
+
});
|
|
181
|
+
expect(result.current.error).not.toBeNull();
|
|
182
|
+
act(() => {
|
|
183
|
+
result.current.dismissError();
|
|
184
|
+
});
|
|
185
|
+
expect(result.current.error).toBeNull();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('does not surface an error when the commit resolves', async () => {
|
|
189
|
+
const { result } = renderHook(() => useConsoleActionQueue());
|
|
190
|
+
const action = makeAction();
|
|
191
|
+
act(() => {
|
|
192
|
+
result.current.enqueue(action);
|
|
193
|
+
});
|
|
194
|
+
await act(async () => {
|
|
195
|
+
jest.advanceTimersByTime(5000);
|
|
196
|
+
await flushMicrotasks();
|
|
197
|
+
});
|
|
198
|
+
expect(result.current.error).toBeNull();
|
|
199
|
+
});
|
|
117
200
|
});
|