github-issue-tower-defence-management 1.152.2 → 1.153.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.
- package/CHANGELOG.md +14 -0
- package/README.md +16 -1
- package/bin/adapter/entry-points/cli/index.js +36 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-DR_LIP9n.js → index-BooMHl5Y.js} +1 -1
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/console/webServer.js +59 -23
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +11 -0
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/ownerCallFileCleaner.js +20 -0
- package/bin/adapter/entry-points/handlers/ownerCallFileCleaner.js.map +1 -0
- package/bin/adapter/entry-points/handlers/ownerCallFileStore.js +80 -0
- package/bin/adapter/entry-points/handlers/ownerCallFileStore.js.map +1 -0
- package/bin/domain/usecases/intmux/OwnerCallFile.js +52 -0
- package/bin/domain/usecases/intmux/OwnerCallFile.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +189 -0
- package/src/adapter/entry-points/cli/index.ts +75 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +1 -4
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +80 -4
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +0 -1
- package/src/adapter/entry-points/console/ui-dist/assets/{index-DR_LIP9n.js → index-BooMHl5Y.js} +1 -1
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/console/webServer.test.ts +317 -0
- package/src/adapter/entry-points/console/webServer.ts +86 -26
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +35 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +15 -0
- package/src/adapter/entry-points/handlers/ownerCallFileCleaner.test.ts +109 -0
- package/src/adapter/entry-points/handlers/ownerCallFileCleaner.ts +25 -0
- package/src/adapter/entry-points/handlers/ownerCallFileStore.test.ts +286 -0
- package/src/adapter/entry-points/handlers/ownerCallFileStore.ts +145 -0
- package/src/domain/usecases/intmux/OwnerCallFile.test.ts +184 -0
- package/src/domain/usecases/intmux/OwnerCallFile.ts +80 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts +1 -0
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/ownerCallFileCleaner.d.ts +8 -0
- package/types/adapter/entry-points/handlers/ownerCallFileCleaner.d.ts.map +1 -0
- package/types/adapter/entry-points/handlers/ownerCallFileStore.d.ts +21 -0
- package/types/adapter/entry-points/handlers/ownerCallFileStore.d.ts.map +1 -0
- package/types/domain/usecases/intmux/OwnerCallFile.d.ts +18 -0
- package/types/domain/usecases/intmux/OwnerCallFile.d.ts.map +1 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
2
3
|
import * as path from 'path';
|
|
3
4
|
import YAML from 'yaml';
|
|
4
5
|
|
|
@@ -21,6 +22,8 @@ import {
|
|
|
21
22
|
import { StartPreparationUseCase } from '../../../domain/usecases/StartPreparationUseCase';
|
|
22
23
|
import { NotifyFinishedIssuePreparationUseCase } from '../../../domain/usecases/NotifyFinishedIssuePreparationUseCase';
|
|
23
24
|
import { CheckIssueReviewReadinessUseCase } from '../../../domain/usecases/CheckIssueReviewReadinessUseCase';
|
|
25
|
+
import { ownerCallFileRelativePath } from '../../../domain/usecases/intmux/OwnerCallFile';
|
|
26
|
+
import { toTmuxSessionName } from '../../../domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase';
|
|
24
27
|
|
|
25
28
|
jest.mock('../../../domain/usecases/StartPreparationUseCase');
|
|
26
29
|
jest.mock('../../../domain/usecases/NotifyFinishedIssuePreparationUseCase');
|
|
@@ -2371,6 +2374,192 @@ mysteryKey: 'value'
|
|
|
2371
2374
|
});
|
|
2372
2375
|
});
|
|
2373
2376
|
|
|
2377
|
+
describe('ownerCallFileAppend and ownerCallFileDelete', () => {
|
|
2378
|
+
const issueUrl = 'https://github.com/OWNER/REPO/issues/1';
|
|
2379
|
+
const otherIssueUrl = 'https://github.com/OWNER/REPO/issues/2';
|
|
2380
|
+
const sessionName = toTmuxSessionName(issueUrl);
|
|
2381
|
+
let ownerCallDataDir = '';
|
|
2382
|
+
let bodyFilePath = '';
|
|
2383
|
+
|
|
2384
|
+
beforeEach(() => {
|
|
2385
|
+
ownerCallDataDir = fs.mkdtempSync(
|
|
2386
|
+
path.join(os.tmpdir(), 'owner-call-cli-'),
|
|
2387
|
+
);
|
|
2388
|
+
bodyFilePath = path.join(ownerCallDataDir, 'body.txt');
|
|
2389
|
+
fs.writeFileSync(bodyFilePath, ' the call body\n\nsecond paragraph\n');
|
|
2390
|
+
});
|
|
2391
|
+
|
|
2392
|
+
afterEach(() => {
|
|
2393
|
+
fs.rmSync(ownerCallDataDir, { recursive: true, force: true });
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2396
|
+
// The in-tmux-by-human data the scheduled run writes into the same
|
|
2397
|
+
// directory serveWeb serves: one file per project, each listing the
|
|
2398
|
+
// sessions of that project by their raw issue url.
|
|
2399
|
+
type InTmuxByHumanProjectData = {
|
|
2400
|
+
projectCode: string;
|
|
2401
|
+
sessionIssueUrls: string[];
|
|
2402
|
+
};
|
|
2403
|
+
|
|
2404
|
+
const writeInTmuxByHumanData = (
|
|
2405
|
+
projects: InTmuxByHumanProjectData[],
|
|
2406
|
+
): void => {
|
|
2407
|
+
for (const project of projects) {
|
|
2408
|
+
fs.writeFileSync(
|
|
2409
|
+
path.join(ownerCallDataDir, `${project.projectCode}.v4.json`),
|
|
2410
|
+
`${JSON.stringify({
|
|
2411
|
+
version: 4,
|
|
2412
|
+
overviewUrl: 'https://github.com/orgs/OWNER/projects/1',
|
|
2413
|
+
tdpmConsoleUrl: 'http://localhost/projects/code',
|
|
2414
|
+
newIssueUrl: 'https://github.com/OWNER/REPO/issues/new',
|
|
2415
|
+
groups: [
|
|
2416
|
+
{
|
|
2417
|
+
story: 'a story',
|
|
2418
|
+
sessions: project.sessionIssueUrls.map((url) => ({
|
|
2419
|
+
name: url,
|
|
2420
|
+
description: 'an issue title',
|
|
2421
|
+
})),
|
|
2422
|
+
},
|
|
2423
|
+
],
|
|
2424
|
+
})}\n`,
|
|
2425
|
+
);
|
|
2426
|
+
}
|
|
2427
|
+
};
|
|
2428
|
+
|
|
2429
|
+
const projectCodeListing = (
|
|
2430
|
+
projects: InTmuxByHumanProjectData[],
|
|
2431
|
+
url: string,
|
|
2432
|
+
): string | null =>
|
|
2433
|
+
projects.find((project) => project.sessionIssueUrls.includes(url))
|
|
2434
|
+
?.projectCode ?? null;
|
|
2435
|
+
|
|
2436
|
+
const appendedFilePath = (projectCode: string | null): string =>
|
|
2437
|
+
path.join(
|
|
2438
|
+
ownerCallDataDir,
|
|
2439
|
+
ownerCallFileRelativePath(projectCode, sessionName),
|
|
2440
|
+
);
|
|
2441
|
+
|
|
2442
|
+
const runAppend = (calledAt: string): Promise<unknown> =>
|
|
2443
|
+
program.parseAsync([
|
|
2444
|
+
'node',
|
|
2445
|
+
'test',
|
|
2446
|
+
'ownerCallFileAppend',
|
|
2447
|
+
'--session',
|
|
2448
|
+
sessionName,
|
|
2449
|
+
'--calledAt',
|
|
2450
|
+
calledAt,
|
|
2451
|
+
'--body-file',
|
|
2452
|
+
bodyFilePath,
|
|
2453
|
+
'--inTmuxDataDir',
|
|
2454
|
+
ownerCallDataDir,
|
|
2455
|
+
]);
|
|
2456
|
+
|
|
2457
|
+
it('appends the call under the directory of the project whose session list holds the session', async () => {
|
|
2458
|
+
const projects: InTmuxByHumanProjectData[] = [
|
|
2459
|
+
{ projectCode: 'other', sessionIssueUrls: [otherIssueUrl] },
|
|
2460
|
+
{ projectCode: 'umino', sessionIssueUrls: [issueUrl] },
|
|
2461
|
+
];
|
|
2462
|
+
writeInTmuxByHumanData(projects);
|
|
2463
|
+
const stdoutSpy = jest
|
|
2464
|
+
.spyOn(process.stdout, 'write')
|
|
2465
|
+
.mockImplementation(() => true);
|
|
2466
|
+
|
|
2467
|
+
await runAppend('2026-08-14T04:22:28Z');
|
|
2468
|
+
|
|
2469
|
+
const documents = YAML.parseAllDocuments(
|
|
2470
|
+
fs.readFileSync(
|
|
2471
|
+
appendedFilePath(projectCodeListing(projects, issueUrl)),
|
|
2472
|
+
'utf-8',
|
|
2473
|
+
),
|
|
2474
|
+
);
|
|
2475
|
+
expect(documents).toHaveLength(1);
|
|
2476
|
+
expect(documents[0].toJS()).toEqual({
|
|
2477
|
+
sessionName,
|
|
2478
|
+
calledAt: '2026-08-14T04:22:28Z',
|
|
2479
|
+
body: ' the call body\n\nsecond paragraph\n',
|
|
2480
|
+
});
|
|
2481
|
+
expect(stdoutSpy).not.toHaveBeenCalled();
|
|
2482
|
+
|
|
2483
|
+
stdoutSpy.mockRestore();
|
|
2484
|
+
});
|
|
2485
|
+
|
|
2486
|
+
it('appends the call under NA when no project lists the session', async () => {
|
|
2487
|
+
const projects: InTmuxByHumanProjectData[] = [
|
|
2488
|
+
{ projectCode: 'umino', sessionIssueUrls: [otherIssueUrl] },
|
|
2489
|
+
];
|
|
2490
|
+
writeInTmuxByHumanData(projects);
|
|
2491
|
+
|
|
2492
|
+
await runAppend('2026-08-14T04:22:28Z');
|
|
2493
|
+
|
|
2494
|
+
expect(projectCodeListing(projects, issueUrl)).toBeNull();
|
|
2495
|
+
expect(fs.existsSync(appendedFilePath(null))).toBe(true);
|
|
2496
|
+
});
|
|
2497
|
+
|
|
2498
|
+
it('rejects a calledAt that is not a UTC ISO-8601 timestamp with second precision', async () => {
|
|
2499
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
2500
|
+
const processExitSpy = jest
|
|
2501
|
+
.spyOn(process, 'exit')
|
|
2502
|
+
.mockImplementation(() => {
|
|
2503
|
+
throw new Error('process.exit called');
|
|
2504
|
+
});
|
|
2505
|
+
|
|
2506
|
+
await expect(runAppend('2026-08-14 04:22:28')).rejects.toThrow(
|
|
2507
|
+
'process.exit called',
|
|
2508
|
+
);
|
|
2509
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
2510
|
+
expect(fs.existsSync(appendedFilePath(null))).toBe(false);
|
|
2511
|
+
|
|
2512
|
+
consoleErrorSpy.mockRestore();
|
|
2513
|
+
processExitSpy.mockRestore();
|
|
2514
|
+
});
|
|
2515
|
+
|
|
2516
|
+
const runDelete = (): Promise<unknown> =>
|
|
2517
|
+
program.parseAsync([
|
|
2518
|
+
'node',
|
|
2519
|
+
'test',
|
|
2520
|
+
'ownerCallFileDelete',
|
|
2521
|
+
'--session',
|
|
2522
|
+
sessionName,
|
|
2523
|
+
'--inTmuxDataDir',
|
|
2524
|
+
ownerCallDataDir,
|
|
2525
|
+
]);
|
|
2526
|
+
|
|
2527
|
+
it('deletes the file of the session whichever project directory holds it', async () => {
|
|
2528
|
+
const projects: InTmuxByHumanProjectData[] = [
|
|
2529
|
+
{ projectCode: 'umino', sessionIssueUrls: [issueUrl] },
|
|
2530
|
+
];
|
|
2531
|
+
writeInTmuxByHumanData(projects);
|
|
2532
|
+
await runAppend('2026-08-14T04:22:28Z');
|
|
2533
|
+
const writtenFilePath = appendedFilePath(
|
|
2534
|
+
projectCodeListing(projects, issueUrl),
|
|
2535
|
+
);
|
|
2536
|
+
expect(fs.existsSync(writtenFilePath)).toBe(true);
|
|
2537
|
+
|
|
2538
|
+
await runDelete();
|
|
2539
|
+
|
|
2540
|
+
expect(fs.existsSync(writtenFilePath)).toBe(false);
|
|
2541
|
+
});
|
|
2542
|
+
|
|
2543
|
+
it('deletes the file left behind under the project the session has since left', async () => {
|
|
2544
|
+
writeInTmuxByHumanData([
|
|
2545
|
+
{ projectCode: 'umino', sessionIssueUrls: [issueUrl] },
|
|
2546
|
+
]);
|
|
2547
|
+
await runAppend('2026-08-14T04:22:28Z');
|
|
2548
|
+
writeInTmuxByHumanData([
|
|
2549
|
+
{ projectCode: 'umino', sessionIssueUrls: [] },
|
|
2550
|
+
{ projectCode: 'other', sessionIssueUrls: [issueUrl] },
|
|
2551
|
+
]);
|
|
2552
|
+
|
|
2553
|
+
await runDelete();
|
|
2554
|
+
|
|
2555
|
+
expect(fs.existsSync(appendedFilePath('umino'))).toBe(false);
|
|
2556
|
+
});
|
|
2557
|
+
|
|
2558
|
+
it('succeeds when the file to delete is already absent', async () => {
|
|
2559
|
+
await expect(runDelete()).resolves.toBeDefined();
|
|
2560
|
+
});
|
|
2561
|
+
});
|
|
2562
|
+
|
|
2374
2563
|
describe('runCliProgram', () => {
|
|
2375
2564
|
const mockCheckUseCaseRejectingWith = (error: Error): void => {
|
|
2376
2565
|
const MockedCheckUseCase = jest.mocked(CheckIssueReviewReadinessUseCase);
|
|
@@ -63,6 +63,12 @@ import {
|
|
|
63
63
|
loadLiveSessionOauthTokenSelectionSettings,
|
|
64
64
|
resolveFleetConfigFilePath,
|
|
65
65
|
} from './fleetConfig';
|
|
66
|
+
import { isOwnerCallCalledAtValid } from '../../../domain/usecases/intmux/OwnerCallFile';
|
|
67
|
+
import {
|
|
68
|
+
ownerCallFileAppend,
|
|
69
|
+
ownerCallFileDeleteInEveryProject,
|
|
70
|
+
ownerCallProjectCodeInInTmuxByHumanData,
|
|
71
|
+
} from '../handlers/ownerCallFileStore';
|
|
66
72
|
|
|
67
73
|
type StartDaemonOptions = {
|
|
68
74
|
projectUrl?: string;
|
|
@@ -102,6 +108,18 @@ type ServeWebOptions = {
|
|
|
102
108
|
dashboardProjectNames?: string;
|
|
103
109
|
};
|
|
104
110
|
|
|
111
|
+
type OwnerCallFileAppendOptions = {
|
|
112
|
+
session: string;
|
|
113
|
+
calledAt: string;
|
|
114
|
+
bodyFile: string;
|
|
115
|
+
inTmuxDataDir?: string;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
type OwnerCallFileDeleteOptions = {
|
|
119
|
+
session: string;
|
|
120
|
+
inTmuxDataDir?: string;
|
|
121
|
+
};
|
|
122
|
+
|
|
105
123
|
const DEFAULT_IN_TMUX_DATA_DIR =
|
|
106
124
|
'/home/hiromi/0_workspaces/workspace1/jsonpub/in-tmux-by-human';
|
|
107
125
|
|
|
@@ -1149,6 +1167,63 @@ program
|
|
|
1149
1167
|
}
|
|
1150
1168
|
});
|
|
1151
1169
|
|
|
1170
|
+
program
|
|
1171
|
+
.command('ownerCallFileAppend')
|
|
1172
|
+
.description(
|
|
1173
|
+
'Append one owner call as a YAML document to the per-session owner call file. The project the session belongs to is resolved from the in-tmux-by-human data in the data directory, and a session no project lists is written under NA. The file is created when it does not exist, and every later call is appended after the existing documents so the oldest call stays first. Nothing is written to stdout on success.',
|
|
1174
|
+
)
|
|
1175
|
+
.requiredOption('--session <name>', 'tmux session name that raised the call')
|
|
1176
|
+
.requiredOption(
|
|
1177
|
+
'--calledAt <timestamp>',
|
|
1178
|
+
'Time the call was raised, as UTC ISO-8601 with second precision and a trailing Z',
|
|
1179
|
+
)
|
|
1180
|
+
.requiredOption(
|
|
1181
|
+
'--body-file <path>',
|
|
1182
|
+
'Path to the file holding the call body; the body is read from a file because it is multi-line and can be long',
|
|
1183
|
+
)
|
|
1184
|
+
.option(
|
|
1185
|
+
'--inTmuxDataDir <path>',
|
|
1186
|
+
`Directory the owner call files are written under, the same directory serveWeb serves them from (default: ${DEFAULT_IN_TMUX_DATA_DIR})`,
|
|
1187
|
+
)
|
|
1188
|
+
.action((options: OwnerCallFileAppendOptions) => {
|
|
1189
|
+
if (!isOwnerCallCalledAtValid(options.calledAt)) {
|
|
1190
|
+
console.error(
|
|
1191
|
+
'--calledAt must be a UTC ISO-8601 timestamp with second precision and a trailing Z, for example 2026-08-14T04:22:28Z',
|
|
1192
|
+
);
|
|
1193
|
+
return process.exit(1);
|
|
1194
|
+
}
|
|
1195
|
+
const dataDir = options.inTmuxDataDir ?? DEFAULT_IN_TMUX_DATA_DIR;
|
|
1196
|
+
ownerCallFileAppend({
|
|
1197
|
+
dataDir,
|
|
1198
|
+
projectCode: ownerCallProjectCodeInInTmuxByHumanData(
|
|
1199
|
+
dataDir,
|
|
1200
|
+
options.session,
|
|
1201
|
+
),
|
|
1202
|
+
ownerCall: {
|
|
1203
|
+
sessionName: options.session,
|
|
1204
|
+
calledAt: options.calledAt,
|
|
1205
|
+
body: fs.readFileSync(options.bodyFile, 'utf-8'),
|
|
1206
|
+
},
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
program
|
|
1211
|
+
.command('ownerCallFileDelete')
|
|
1212
|
+
.description(
|
|
1213
|
+
'Delete the owner call file of one session, whichever project directory holds it, so a session that moved between projects after its call was appended leaves nothing behind. It succeeds when the file is already absent, so a reset that runs twice is not an error. Nothing is written to stdout on success.',
|
|
1214
|
+
)
|
|
1215
|
+
.requiredOption('--session <name>', 'tmux session name that raised the call')
|
|
1216
|
+
.option(
|
|
1217
|
+
'--inTmuxDataDir <path>',
|
|
1218
|
+
`Directory the owner call files are written under, the same directory serveWeb serves them from (default: ${DEFAULT_IN_TMUX_DATA_DIR})`,
|
|
1219
|
+
)
|
|
1220
|
+
.action((options: OwnerCallFileDeleteOptions) => {
|
|
1221
|
+
ownerCallFileDeleteInEveryProject({
|
|
1222
|
+
dataDir: options.inTmuxDataDir ?? DEFAULT_IN_TMUX_DATA_DIR,
|
|
1223
|
+
sessionName: options.session,
|
|
1224
|
+
});
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1152
1227
|
export const reportFatalErrorAndExit = (error: unknown): void => {
|
|
1153
1228
|
console.error(error);
|
|
1154
1229
|
process.exit(1);
|
|
@@ -51,7 +51,6 @@ export type ConsoleItemDetailProps = {
|
|
|
51
51
|
bodyIsLoading: boolean;
|
|
52
52
|
bodyError: string | null;
|
|
53
53
|
comments: ConsoleComment[];
|
|
54
|
-
hasPostedComment?: boolean;
|
|
55
54
|
commentsAreLoading: boolean;
|
|
56
55
|
commentsError: string | null;
|
|
57
56
|
files: ConsoleChangedFile[];
|
|
@@ -83,7 +82,6 @@ export const ConsoleItemDetail = ({
|
|
|
83
82
|
bodyIsLoading,
|
|
84
83
|
bodyError,
|
|
85
84
|
comments,
|
|
86
|
-
hasPostedComment = false,
|
|
87
85
|
commentsAreLoading,
|
|
88
86
|
commentsError,
|
|
89
87
|
files,
|
|
@@ -277,10 +275,9 @@ export const ConsoleItemDetail = ({
|
|
|
277
275
|
)}
|
|
278
276
|
|
|
279
277
|
<ConsolePanel
|
|
280
|
-
key={hasPostedComment ? 'comments-with-posted' : 'comments'}
|
|
281
278
|
title="Comments"
|
|
282
279
|
count={commentsCount}
|
|
283
|
-
defaultCollapsed={item.isPr
|
|
280
|
+
defaultCollapsed={item.isPr}
|
|
284
281
|
>
|
|
285
282
|
<ConsoleCommentList
|
|
286
283
|
comments={comments}
|
|
@@ -83,6 +83,20 @@ const buildOperations = (): ConsoleOperationsApi => ({
|
|
|
83
83
|
addInlineReviewComment: jest.fn(async () => {}),
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
+
const findCommentsPanelToggle = (container: HTMLElement): HTMLElement => {
|
|
87
|
+
const toggle = Array.from(
|
|
88
|
+
container.querySelectorAll('.console-panel-toggle'),
|
|
89
|
+
).find((element) =>
|
|
90
|
+
element
|
|
91
|
+
.querySelector('.console-panel-title')
|
|
92
|
+
?.textContent?.startsWith('Comments'),
|
|
93
|
+
);
|
|
94
|
+
if (toggle === undefined) {
|
|
95
|
+
throw new Error('Comments panel toggle is not rendered');
|
|
96
|
+
}
|
|
97
|
+
return toggle as HTMLElement;
|
|
98
|
+
};
|
|
99
|
+
|
|
86
100
|
describe('ConsoleItemDetailContainer', () => {
|
|
87
101
|
it('queues the review action and commits it through the operations api for a PR item', async () => {
|
|
88
102
|
const operations = buildOperations();
|
|
@@ -182,7 +196,7 @@ describe('ConsoleItemDetailContainer', () => {
|
|
|
182
196
|
).toBe(0);
|
|
183
197
|
});
|
|
184
198
|
|
|
185
|
-
it('
|
|
199
|
+
it('leaves the collapsed comments panel of a pull request item collapsed after a comment is posted', async () => {
|
|
186
200
|
const operations = buildOperations();
|
|
187
201
|
operations.addComment = jest.fn(async (_item, body) => ({
|
|
188
202
|
author: 'HiromiShikata',
|
|
@@ -205,16 +219,78 @@ describe('ConsoleItemDetailContainer', () => {
|
|
|
205
219
|
/>,
|
|
206
220
|
);
|
|
207
221
|
|
|
222
|
+
await waitFor(() => {
|
|
223
|
+
expect(findCommentsPanelToggle(container).textContent).toContain(
|
|
224
|
+
'Comments (0)',
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
expect(
|
|
228
|
+
findCommentsPanelToggle(container).getAttribute('aria-expanded'),
|
|
229
|
+
).toBe('false');
|
|
230
|
+
|
|
208
231
|
fireEvent.change(getByPlaceholderText('Leave a comment…'), {
|
|
209
232
|
target: { value: 'Posted from the pull request item.' },
|
|
210
233
|
});
|
|
211
234
|
fireEvent.click(getByText('Comment'));
|
|
212
235
|
|
|
213
236
|
await waitFor(() => {
|
|
214
|
-
expect(
|
|
215
|
-
|
|
216
|
-
)
|
|
237
|
+
expect(findCommentsPanelToggle(container).textContent).toContain(
|
|
238
|
+
'Comments (1)',
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
expect(
|
|
242
|
+
findCommentsPanelToggle(container).getAttribute('aria-expanded'),
|
|
243
|
+
).toBe('false');
|
|
244
|
+
expect(container.querySelector('.console-comment-list')).toBeNull();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('keeps a reader-collapsed comments panel collapsed after a comment is posted on an issue item', async () => {
|
|
248
|
+
const operations = buildOperations();
|
|
249
|
+
operations.addComment = jest.fn(async (_item, body) => ({
|
|
250
|
+
author: 'HiromiShikata',
|
|
251
|
+
body,
|
|
252
|
+
createdAt: '2026-06-19T11:58:00.000Z',
|
|
253
|
+
}));
|
|
254
|
+
const { container, getByPlaceholderText, getByText } = render(
|
|
255
|
+
<ConsoleItemDetailContainer
|
|
256
|
+
tab="unread"
|
|
257
|
+
item={issueItem}
|
|
258
|
+
caches={buildCaches()}
|
|
259
|
+
operations={operations}
|
|
260
|
+
statusOptions={consoleStatusOptionsFixture}
|
|
261
|
+
storyOptions={consoleStoryOptionsFixture}
|
|
262
|
+
storyColors={consoleStoryColorsFixture}
|
|
263
|
+
storyName="TDPM Console port"
|
|
264
|
+
overlayStatus={null}
|
|
265
|
+
now={Date.parse('2026-06-19T12:00:00.000Z')}
|
|
266
|
+
onQueueAction={jest.fn()}
|
|
267
|
+
/>,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
await waitFor(() => {
|
|
271
|
+
expect(findCommentsPanelToggle(container).textContent).toContain(
|
|
272
|
+
'Comments (0)',
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
fireEvent.click(findCommentsPanelToggle(container));
|
|
276
|
+
expect(
|
|
277
|
+
findCommentsPanelToggle(container).getAttribute('aria-expanded'),
|
|
278
|
+
).toBe('false');
|
|
279
|
+
|
|
280
|
+
fireEvent.change(getByPlaceholderText('Leave a comment…'), {
|
|
281
|
+
target: { value: 'Posted from the issue item.' },
|
|
217
282
|
});
|
|
283
|
+
fireEvent.click(getByText('Comment'));
|
|
284
|
+
|
|
285
|
+
await waitFor(() => {
|
|
286
|
+
expect(findCommentsPanelToggle(container).textContent).toContain(
|
|
287
|
+
'Comments (1)',
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
expect(
|
|
291
|
+
findCommentsPanelToggle(container).getAttribute('aria-expanded'),
|
|
292
|
+
).toBe('false');
|
|
293
|
+
expect(container.querySelector('.console-comment-list')).toBeNull();
|
|
218
294
|
});
|
|
219
295
|
|
|
220
296
|
it('shows Approve for an issue item from the generated related open pull request urls before the related pull requests are fetched', () => {
|
|
@@ -171,7 +171,6 @@ export const ConsoleItemDetailContainer = ({
|
|
|
171
171
|
bodyIsLoading={detail.bodyIsLoading}
|
|
172
172
|
bodyError={detail.bodyError}
|
|
173
173
|
comments={mergePostedComments(detail.comments, postedComments)}
|
|
174
|
-
hasPostedComment={postedComments.length > 0}
|
|
175
174
|
commentsAreLoading={detail.commentsAreLoading}
|
|
176
175
|
commentsError={detail.commentsError}
|
|
177
176
|
files={detail.files}
|