github-issue-tower-defence-management 1.167.0 → 1.168.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 (27) hide show
  1. package/.github/workflows/console-ui.yml +1 -0
  2. package/CHANGELOG.md +13 -0
  3. package/bin/adapter/entry-points/console/ui-dist/assets/index-DkAuY63u.js +86 -0
  4. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  5. package/bin/adapter/entry-points/console/ui-dist/sw.js +60 -0
  6. package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js +7 -1
  7. package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/adapter/ci/consoleUiWorkflowTimeout.test.ts +61 -0
  10. package/src/adapter/entry-points/console/ui/public/sw.js +60 -0
  11. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +9 -0
  12. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +31 -0
  13. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.tsx +8 -1
  14. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +111 -15
  15. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +100 -15
  16. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +70 -1
  17. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +42 -6
  18. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +2 -0
  19. package/src/adapter/entry-points/console/ui/src/main.tsx +6 -0
  20. package/src/adapter/entry-points/console/ui-dist/assets/index-DkAuY63u.js +86 -0
  21. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  22. package/src/adapter/entry-points/console/ui-dist/sw.js +60 -0
  23. package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +13 -0
  24. package/src/domain/usecases/console/GenerateConsoleListsUseCase.ts +6 -1
  25. package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts.map +1 -1
  26. package/bin/adapter/entry-points/console/ui-dist/assets/index-CrSqMAVR.js +0 -86
  27. package/src/adapter/entry-points/console/ui-dist/assets/index-CrSqMAVR.js +0 -86
@@ -25,7 +25,40 @@ const mockFetchFailureOnce = (status: number, rawBody: string): jest.Mock => {
25
25
  return fetchMock;
26
26
  };
27
27
 
28
+ type MockCacheEntry = { json: jest.Mock };
29
+ type MockCache = {
30
+ match: jest.Mock<Promise<MockCacheEntry | undefined>>;
31
+ put: jest.Mock<Promise<void>>;
32
+ };
33
+
34
+ const installMockCaches = (
35
+ matchResult: MockCacheEntry | undefined,
36
+ ): MockCache => {
37
+ const mockCache: MockCache = {
38
+ match: jest.fn(async () => matchResult),
39
+ put: jest.fn(async () => undefined),
40
+ };
41
+ Object.defineProperty(global, 'caches', {
42
+ value: { open: jest.fn(async () => mockCache) },
43
+ writable: true,
44
+ configurable: true,
45
+ });
46
+ return mockCache;
47
+ };
48
+
49
+ const removeMockCaches = (): void => {
50
+ Reflect.deleteProperty(global as Record<string, unknown>, 'caches');
51
+ };
52
+
28
53
  describe('createConsoleApiClient', () => {
54
+ beforeEach(() => {
55
+ removeMockCaches();
56
+ });
57
+
58
+ afterEach(() => {
59
+ removeMockCaches();
60
+ });
61
+
29
62
  it('reads the item body from the api root without a token query', async () => {
30
63
  const fetchMock = mockFetchOnce({ body: '# Title' });
31
64
  const client = createConsoleApiClient();
@@ -243,13 +276,49 @@ describe('createConsoleApiClient', () => {
243
276
  });
244
277
  });
245
278
 
246
- it('throws on a non-ok response', async () => {
279
+ it('throws on a non-ok response when no cache is available', async () => {
247
280
  mockFetchOnce({}, false);
248
281
  const client = createConsoleApiClient();
249
282
  await expect(
250
283
  client.fetchComments('https://github.com/o/r/issues/1'),
251
284
  ).rejects.toThrow('HTTP 500');
252
285
  });
286
+
287
+ it('returns cached comments when the network fetch rejects', async () => {
288
+ const cachedBody = {
289
+ comments: [
290
+ {
291
+ author: 'alice',
292
+ body: 'hello',
293
+ createdAt: '2026-06-19T00:00:00.000Z',
294
+ },
295
+ ],
296
+ };
297
+ installMockCaches({ json: jest.fn(async () => cachedBody) });
298
+ global.fetch = jest
299
+ .fn()
300
+ .mockRejectedValue(new Error('offline')) as unknown as typeof fetch;
301
+
302
+ const client = createConsoleApiClient();
303
+ const comments = await client.fetchComments(
304
+ 'https://github.com/o/r/issues/1',
305
+ );
306
+ expect(comments).toEqual([
307
+ { author: 'alice', body: 'hello', createdAt: '2026-06-19T00:00:00.000Z' },
308
+ ]);
309
+ });
310
+
311
+ it('throws when the network fails and no cache entry exists', async () => {
312
+ installMockCaches(undefined);
313
+ global.fetch = jest
314
+ .fn()
315
+ .mockRejectedValue(new Error('offline')) as unknown as typeof fetch;
316
+
317
+ const client = createConsoleApiClient();
318
+ await expect(
319
+ client.fetchComments('https://github.com/o/r/issues/1'),
320
+ ).rejects.toThrow('offline');
321
+ });
253
322
  });
254
323
 
255
324
  describe('postConsoleOperation', () => {
@@ -70,17 +70,53 @@ const getNumber = (value: unknown): number =>
70
70
 
71
71
  const getBoolean = (value: unknown): boolean => value === true;
72
72
 
73
+ const CONSOLE_API_CACHE_NAME = 'console-api-v1';
74
+
73
75
  const requestJson = async (
74
76
  apiPath: string,
75
77
  resourceUrl: string,
76
78
  ): Promise<unknown> => {
77
- const response = await fetch(
78
- `${apiPath}?url=${encodeURIComponent(resourceUrl)}`,
79
- );
80
- if (!response.ok) {
81
- throw new Error(`HTTP ${response.status}`);
79
+ const url = `${apiPath}?url=${encodeURIComponent(resourceUrl)}`;
80
+ try {
81
+ const response = await fetch(url);
82
+ if (!response.ok) {
83
+ throw new Error(`HTTP ${response.status}`);
84
+ }
85
+ const payload: unknown = await response.json();
86
+ try {
87
+ if ('caches' in globalThis) {
88
+ globalThis.caches
89
+ .open(CONSOLE_API_CACHE_NAME)
90
+ .then((cache) =>
91
+ cache.put(
92
+ url,
93
+ new Response(JSON.stringify(payload), {
94
+ headers: { 'Content-Type': 'application/json' },
95
+ }),
96
+ ),
97
+ )
98
+ .catch((e: unknown) => {
99
+ console.warn('Failed to persist API response to cache:', e);
100
+ });
101
+ }
102
+ } catch (e: unknown) {
103
+ console.warn('Failed to access cache storage:', e);
104
+ }
105
+ return payload;
106
+ } catch (e: unknown) {
107
+ try {
108
+ if ('caches' in globalThis) {
109
+ const cache = await globalThis.caches.open(CONSOLE_API_CACHE_NAME);
110
+ const cached = await cache.match(url);
111
+ if (cached !== undefined) {
112
+ return cached.json();
113
+ }
114
+ }
115
+ } catch {
116
+ // cache read failure, fall through to original error
117
+ }
118
+ throw e;
82
119
  }
83
- return response.json();
84
120
  };
85
121
 
86
122
  const parseComments = (payload: unknown): ConsoleComment[] => {
@@ -126,6 +126,7 @@ export const ConsolePage = () => {
126
126
  const statusOptions = activeSnapshot?.statusOptions ?? [];
127
127
  const storyOptions = activeSnapshot?.storyOptions ?? [];
128
128
  const generatedAt = activeSnapshot?.generatedAt ?? null;
129
+ const fromCache = activeSnapshot?.fromCache ?? false;
129
130
 
130
131
  const selectedItem = useMemo<ConsoleListItem | null>(() => {
131
132
  if (selectedItemKey === null || activeSnapshot === null) {
@@ -274,6 +275,7 @@ export const ConsolePage = () => {
274
275
  counts={counts}
275
276
  pjcode={pjcode}
276
277
  generatedAt={generatedAt}
278
+ fromCache={fromCache}
277
279
  tabHref={navigation.tabHref}
278
280
  onSelectTab={navigation.selectTab}
279
281
  />
@@ -3,6 +3,12 @@ import { createRoot } from 'react-dom/client';
3
3
  import { ConsolePage } from '@/features/console/pages/ConsolePage';
4
4
  import './index.css';
5
5
 
6
+ if ('serviceWorker' in navigator) {
7
+ navigator.serviceWorker.register('/sw.js').catch((e: unknown) => {
8
+ console.warn('Service worker registration failed:', e);
9
+ });
10
+ }
11
+
6
12
  const container = document.getElementById('root');
7
13
  if (container === null) {
8
14
  throw new Error('Root container #root not found');