@gasboost/client 0.1.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 (63) hide show
  1. package/dist/AppsScriptClient.d.ts +30 -0
  2. package/dist/AppsScriptClient.js +43 -0
  3. package/dist/AppsScriptJob.d.ts +28 -0
  4. package/dist/AppsScriptJob.js +84 -0
  5. package/dist/AppsScriptJobQueue.d.ts +14 -0
  6. package/dist/AppsScriptJobQueue.js +40 -0
  7. package/dist/AppsScriptJobRunner.d.ts +20 -0
  8. package/dist/AppsScriptJobRunner.js +93 -0
  9. package/dist/AppsScriptJobStore.d.ts +7 -0
  10. package/dist/AppsScriptJobStore.js +9 -0
  11. package/dist/google.d.ts +36 -0
  12. package/dist/google.js +8 -0
  13. package/dist/index.d.ts +4 -0
  14. package/dist/index.js +7 -0
  15. package/dist/job/AppsScriptJob.d.ts +28 -0
  16. package/dist/job/AppsScriptJob.js +84 -0
  17. package/dist/job/AppsScriptJobQueue.d.ts +14 -0
  18. package/dist/job/AppsScriptJobQueue.js +40 -0
  19. package/dist/job/AppsScriptJobRunner.d.ts +20 -0
  20. package/dist/job/AppsScriptJobRunner.js +93 -0
  21. package/dist/job/AppsScriptJobStore.d.ts +7 -0
  22. package/dist/job/AppsScriptJobStore.js +9 -0
  23. package/dist/navigation/AppsScriptContainer.d.ts +9 -0
  24. package/dist/navigation/AppsScriptContainer.js +38 -0
  25. package/dist/navigation/AppsScriptHistoryPipeline.d.ts +9 -0
  26. package/dist/navigation/AppsScriptHistoryPipeline.js +28 -0
  27. package/dist/navigation/AppsScriptIframe.d.ts +9 -0
  28. package/dist/navigation/AppsScriptIframe.js +45 -0
  29. package/dist/navigation/HashProperty.d.ts +7 -0
  30. package/dist/navigation/HashProperty.js +27 -0
  31. package/dist/navigation/NavigationEntry.d.ts +9 -0
  32. package/dist/navigation/NavigationEntry.js +54 -0
  33. package/dist/navigation/NavigationLocation.d.ts +8 -0
  34. package/dist/navigation/NavigationLocation.js +43 -0
  35. package/package.json +20 -0
  36. package/src/AppsScriptClient.ts +81 -0
  37. package/src/google.ts +55 -0
  38. package/src/index.ts +4 -0
  39. package/src/job/AppsScriptJob.ts +85 -0
  40. package/src/job/AppsScriptJobQueue.ts +48 -0
  41. package/src/job/AppsScriptJobRunner.ts +107 -0
  42. package/src/job/AppsScriptJobStore.ts +16 -0
  43. package/src/navigation/AppsScriptContainer.ts +47 -0
  44. package/src/navigation/AppsScriptHistoryPipeline.ts +29 -0
  45. package/src/navigation/AppsScriptIframe.ts +63 -0
  46. package/src/navigation/HashProperty.ts +20 -0
  47. package/src/navigation/NavigationEntry.ts +82 -0
  48. package/src/navigation/NavigationLocation.ts +54 -0
  49. package/tests/AppsScriptClient.spec.ts +322 -0
  50. package/tests/AppsScriptClient.type.spec.ts +52 -0
  51. package/tests/AppsScriptContainer.spec.ts +502 -0
  52. package/tests/AppsScriptHistoryPipeline.spec.ts +303 -0
  53. package/tests/AppsScriptIframe.spec.ts +452 -0
  54. package/tests/AppsScriptJob.spec.ts +83 -0
  55. package/tests/AppsScriptJobQueue.spec.ts +125 -0
  56. package/tests/AppsScriptJobRunner.spec.ts +361 -0
  57. package/tests/HashProperty.spec.ts +29 -0
  58. package/tests/NavigationEntry.spec.ts +507 -0
  59. package/tests/NavigationLocation.spec.ts +189 -0
  60. package/tests/setup.ts +15 -0
  61. package/tsconfig.build.json +14 -0
  62. package/tsconfig.json +6 -0
  63. package/vitest.config.mts +8 -0
@@ -0,0 +1,452 @@
1
+ // @vitest-environment jsdom
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { AppsScriptIframe } from "../src/navigation/AppsScriptIframe";
4
+ import { HashProperty } from "../src/navigation/HashProperty";
5
+ import { NavigationEntry } from "../src/navigation/NavigationEntry";
6
+ import { NavigationLocation } from "../src/navigation/NavigationLocation";
7
+
8
+ describe("AppsScriptIframe", () => {
9
+ beforeEach(() => {
10
+ history.replaceState({}, "", "/");
11
+ window.location.hash = "";
12
+ vi.restoreAllMocks();
13
+ });
14
+
15
+ describe("constructor", () => {
16
+ it("window.location.hash から現在の hash を読み込める", () => {
17
+ window.location.hash = "#/users";
18
+
19
+ const iframe = new AppsScriptIframe();
20
+
21
+ expect(iframe.current().location.hash.normalize()).toBe("#/users");
22
+ });
23
+
24
+ it("hash が空の場合はルートとして扱える", () => {
25
+ window.location.hash = "";
26
+
27
+ const iframe = new AppsScriptIframe();
28
+
29
+ expect(iframe.current().location.hash.normalize()).toBe("#/");
30
+ });
31
+
32
+ it("history.state を現在の state として読み込める", () => {
33
+ history.replaceState(
34
+ {
35
+ selectedUserId: "123",
36
+ },
37
+ "",
38
+ "#/users",
39
+ );
40
+
41
+ const iframe = new AppsScriptIframe();
42
+
43
+ expect(iframe.current().state).toEqual({
44
+ selectedUserId: "123",
45
+ });
46
+ });
47
+
48
+ it("history.state が null の場合は空オブジェクトとして扱える", () => {
49
+ history.replaceState(null, "", "#/users");
50
+
51
+ const iframe = new AppsScriptIframe();
52
+
53
+ expect(iframe.current().state).toEqual({});
54
+ });
55
+
56
+ it("hash 内の query parameter を searchParams として読み込める", () => {
57
+ window.location.hash = "#/users?page=2&sort=name";
58
+
59
+ const iframe = new AppsScriptIframe();
60
+
61
+ expect(iframe.current().location.searchParams.get("page")).toBe("2");
62
+ expect(iframe.current().location.searchParams.get("sort")).toBe("name");
63
+ });
64
+
65
+ it("hash 内の同一キーの複数 query parameter を欠損せず読み込める", () => {
66
+ window.location.hash = "#/users?tag=react&tag=typescript";
67
+
68
+ const iframe = new AppsScriptIframe();
69
+
70
+ expect(iframe.current().location.searchParams.getAll("tag")).toEqual([
71
+ "react",
72
+ "typescript",
73
+ ]);
74
+ });
75
+
76
+ it("hash 内に query parameter がない場合は空の searchParams を保持する", () => {
77
+ window.location.hash = "#/users";
78
+
79
+ const iframe = new AppsScriptIframe();
80
+
81
+ expect(iframe.current().location.searchParams.toString()).toBe("");
82
+ });
83
+ });
84
+
85
+ describe("observe", () => {
86
+ it("hashchange を監視できる", () => {
87
+ const iframe = new AppsScriptIframe();
88
+ const observer = vi.fn();
89
+
90
+ iframe.observe(observer);
91
+
92
+ window.location.hash = "#/users";
93
+ window.dispatchEvent(new HashChangeEvent("hashchange"));
94
+
95
+ expect(observer).toHaveBeenCalled();
96
+ });
97
+
98
+ it("hashchange 発生時に最新の NavigationEntry を通知する", () => {
99
+ const iframe = new AppsScriptIframe();
100
+ const observer = vi.fn();
101
+
102
+ iframe.observe(observer);
103
+
104
+ history.replaceState(
105
+ {
106
+ selectedUserId: "123",
107
+ },
108
+ "",
109
+ "#/users?page=2",
110
+ );
111
+
112
+ window.dispatchEvent(new HashChangeEvent("hashchange"));
113
+
114
+ const entry = observer.mock.calls.at(-1)?.[0] as NavigationEntry;
115
+
116
+ expect(entry.state).toEqual({
117
+ selectedUserId: "123",
118
+ });
119
+
120
+ expect(entry.location.hash.normalize()).toBe("#/users");
121
+ expect(entry.location.searchParams.get("page")).toBe("2");
122
+ });
123
+
124
+ it("hashchange 発生時に自身の現在状態も更新する", () => {
125
+ const iframe = new AppsScriptIframe();
126
+
127
+ iframe.observe(vi.fn());
128
+
129
+ window.location.hash = "#/settings";
130
+ window.dispatchEvent(new HashChangeEvent("hashchange"));
131
+
132
+ expect(iframe.current().location.hash.normalize()).toBe("#/settings");
133
+ });
134
+
135
+ it("popstate を監視できる", () => {
136
+ const iframe = new AppsScriptIframe();
137
+ const observer = vi.fn();
138
+
139
+ iframe.observe(observer);
140
+
141
+ history.replaceState(
142
+ {
143
+ page: 2,
144
+ },
145
+ "",
146
+ "#/users",
147
+ );
148
+
149
+ window.dispatchEvent(
150
+ new PopStateEvent("popstate", {
151
+ state: {
152
+ page: 2,
153
+ },
154
+ }),
155
+ );
156
+
157
+ expect(observer).toHaveBeenCalled();
158
+ });
159
+
160
+ it("popstate 発生時に最新の NavigationEntry を通知する", () => {
161
+ const iframe = new AppsScriptIframe();
162
+ const observer = vi.fn();
163
+
164
+ iframe.observe(observer);
165
+
166
+ history.replaceState(
167
+ {
168
+ page: 2,
169
+ },
170
+ "",
171
+ "#/users?page=2",
172
+ );
173
+
174
+ window.dispatchEvent(
175
+ new PopStateEvent("popstate", {
176
+ state: {
177
+ page: 2,
178
+ },
179
+ }),
180
+ );
181
+
182
+ const entry = observer.mock.calls.at(-1)?.[0] as NavigationEntry;
183
+
184
+ expect(entry.state).toEqual({
185
+ page: 2,
186
+ });
187
+
188
+ expect(entry.location.hash.normalize()).toBe("#/users");
189
+ expect(entry.location.searchParams.get("page")).toBe("2");
190
+ });
191
+
192
+ it("複数回変更された場合は最新の状態を保持する", () => {
193
+ const iframe = new AppsScriptIframe();
194
+
195
+ iframe.observe(vi.fn());
196
+
197
+ window.location.hash = "#/users";
198
+ window.dispatchEvent(new HashChangeEvent("hashchange"));
199
+
200
+ window.location.hash = "#/settings";
201
+ window.dispatchEvent(new HashChangeEvent("hashchange"));
202
+
203
+ expect(iframe.current().location.hash.normalize()).toBe("#/settings");
204
+ });
205
+
206
+ it("監視解除後は hashchange を通知しない", () => {
207
+ const iframe = new AppsScriptIframe();
208
+ const observer = vi.fn();
209
+
210
+ const unsubscribe = iframe.observe(observer);
211
+
212
+ unsubscribe();
213
+
214
+ window.location.hash = "#/users";
215
+ window.dispatchEvent(new HashChangeEvent("hashchange"));
216
+
217
+ expect(observer).not.toHaveBeenCalled();
218
+ });
219
+
220
+ it("監視解除後は popstate を通知しない", () => {
221
+ const iframe = new AppsScriptIframe();
222
+ const observer = vi.fn();
223
+
224
+ const unsubscribe = iframe.observe(observer);
225
+
226
+ unsubscribe();
227
+
228
+ window.dispatchEvent(
229
+ new PopStateEvent("popstate", {
230
+ state: {},
231
+ }),
232
+ );
233
+
234
+ expect(observer).not.toHaveBeenCalled();
235
+ });
236
+ });
237
+
238
+ describe("sync", () => {
239
+ it("異なる NavigationEntry を受け取った場合は iframe に同期する", () => {
240
+ const iframe = new AppsScriptIframe();
241
+
242
+ const entry = createEntry("#/users");
243
+
244
+ iframe.sync(entry);
245
+
246
+ expect(window.location.hash).toBe("#/users");
247
+ });
248
+
249
+ it("同期時に state を iframe history に反映する", () => {
250
+ const iframe = new AppsScriptIframe();
251
+
252
+ const entry = new NavigationEntry(
253
+ {
254
+ selectedUserId: "123",
255
+ },
256
+ new NavigationLocation(
257
+ new HashProperty("#/users"),
258
+ new URLSearchParams(),
259
+ ),
260
+ );
261
+
262
+ iframe.sync(entry);
263
+
264
+ expect(history.state).toEqual({
265
+ selectedUserId: "123",
266
+ });
267
+ });
268
+
269
+ it("同期時に query parameter を hash 内へ反映する", () => {
270
+ const iframe = new AppsScriptIframe();
271
+
272
+ const entry = new NavigationEntry(
273
+ {},
274
+ new NavigationLocation(
275
+ new HashProperty("#/users"),
276
+ new URLSearchParams("page=2&tag=react&tag=typescript"),
277
+ ),
278
+ );
279
+
280
+ iframe.sync(entry);
281
+
282
+ expect(window.location.hash).toBe(
283
+ "#/users?page=2&tag=react&tag=typescript",
284
+ );
285
+ });
286
+
287
+ it("同期時に同一キーの複数 query parameter の順序を保持する", () => {
288
+ const iframe = new AppsScriptIframe();
289
+
290
+ const entry = new NavigationEntry(
291
+ {},
292
+ new NavigationLocation(
293
+ new HashProperty("#/users"),
294
+ new URLSearchParams("tag=typescript&tag=react"),
295
+ ),
296
+ );
297
+
298
+ iframe.sync(entry);
299
+
300
+ expect(window.location.hash).toBe("#/users?tag=typescript&tag=react");
301
+ });
302
+
303
+ it("同期時に hash を正規化する", () => {
304
+ const iframe = new AppsScriptIframe();
305
+
306
+ iframe.sync(createEntry("/users"));
307
+
308
+ expect(window.location.hash).toBe("#/users");
309
+ });
310
+
311
+ it("同期後に自身の現在状態を更新する", () => {
312
+ const iframe = new AppsScriptIframe();
313
+ const entry = createEntry("#/settings");
314
+
315
+ iframe.sync(entry);
316
+
317
+ expect(iframe.current()).toBe(entry);
318
+ });
319
+
320
+ it("同一の NavigationEntry を受け取った場合は history を更新しない", () => {
321
+ history.replaceState({}, "", "#/users");
322
+
323
+ const iframe = new AppsScriptIframe();
324
+
325
+ const replaceState = vi.spyOn(history, "replaceState");
326
+
327
+ iframe.sync(
328
+ new NavigationEntry(
329
+ {},
330
+ new NavigationLocation(
331
+ new HashProperty("/users"),
332
+ new URLSearchParams(),
333
+ ),
334
+ ),
335
+ );
336
+
337
+ expect(replaceState).not.toHaveBeenCalled();
338
+ });
339
+
340
+ it("同じ状態を複数回同期しても history を重複して更新しない", () => {
341
+ const iframe = new AppsScriptIframe();
342
+
343
+ const replaceState = vi.spyOn(history, "replaceState");
344
+
345
+ const entry = createEntry("#/users");
346
+
347
+ iframe.sync(entry);
348
+ iframe.sync(entry);
349
+
350
+ expect(replaceState).toHaveBeenCalledOnce();
351
+ });
352
+
353
+ it("state が異なる場合は location が同じでも同期する", () => {
354
+ history.replaceState(
355
+ {
356
+ page: 1,
357
+ },
358
+ "",
359
+ "#/users",
360
+ );
361
+
362
+ const iframe = new AppsScriptIframe();
363
+
364
+ const replaceState = vi.spyOn(history, "replaceState");
365
+
366
+ iframe.sync(
367
+ new NavigationEntry(
368
+ {
369
+ page: 2,
370
+ },
371
+ new NavigationLocation(
372
+ new HashProperty("#/users"),
373
+ new URLSearchParams(),
374
+ ),
375
+ ),
376
+ );
377
+
378
+ expect(replaceState).toHaveBeenCalledOnce();
379
+ expect(history.state).toEqual({
380
+ page: 2,
381
+ });
382
+ });
383
+
384
+ it("query parameter が異なる場合は hash path が同じでも同期する", () => {
385
+ history.replaceState({}, "", "#/users?page=1");
386
+
387
+ const iframe = new AppsScriptIframe();
388
+
389
+ const replaceState = vi.spyOn(history, "replaceState");
390
+
391
+ iframe.sync(
392
+ new NavigationEntry(
393
+ {},
394
+ new NavigationLocation(
395
+ new HashProperty("#/users"),
396
+ new URLSearchParams("page=2"),
397
+ ),
398
+ ),
399
+ );
400
+
401
+ expect(replaceState).toHaveBeenCalledOnce();
402
+ expect(window.location.hash).toBe("#/users?page=2");
403
+ });
404
+
405
+ it("query parameter のキー順だけが異なる場合は同期しない", () => {
406
+ history.replaceState({}, "", "#/users?page=1&sort=name");
407
+
408
+ const iframe = new AppsScriptIframe();
409
+
410
+ const replaceState = vi.spyOn(history, "replaceState");
411
+
412
+ iframe.sync(
413
+ new NavigationEntry(
414
+ {},
415
+ new NavigationLocation(
416
+ new HashProperty("#/users"),
417
+ new URLSearchParams("sort=name&page=1"),
418
+ ),
419
+ ),
420
+ );
421
+
422
+ expect(replaceState).not.toHaveBeenCalled();
423
+ });
424
+
425
+ it("同一キーの複数値の順序が異なる場合は同期する", () => {
426
+ history.replaceState({}, "", "#/users?tag=react&tag=typescript");
427
+
428
+ const iframe = new AppsScriptIframe();
429
+
430
+ const replaceState = vi.spyOn(history, "replaceState");
431
+
432
+ iframe.sync(
433
+ new NavigationEntry(
434
+ {},
435
+ new NavigationLocation(
436
+ new HashProperty("#/users"),
437
+ new URLSearchParams("tag=typescript&tag=react"),
438
+ ),
439
+ ),
440
+ );
441
+
442
+ expect(replaceState).toHaveBeenCalledOnce();
443
+ });
444
+ });
445
+ });
446
+
447
+ function createEntry(hash = "#/") {
448
+ return new NavigationEntry(
449
+ {},
450
+ new NavigationLocation(new HashProperty(hash), new URLSearchParams()),
451
+ );
452
+ }
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { AppsScriptJob } from "../src/job/AppsScriptJob";
3
+
4
+ describe("AppsScriptJob", () => {
5
+ it("生成直後はpending", () => {
6
+ const resolve = vi.fn();
7
+ const reject = vi.fn();
8
+
9
+ const job = new AppsScriptJob(
10
+ "test",
11
+ async () => "result",
12
+ resolve,
13
+ reject,
14
+ "job-1",
15
+ new Date("2026-09-04T00:00:00.000Z"),
16
+ );
17
+
18
+ expect(job.id).toBe("job-1");
19
+ expect(job.label).toBe("test");
20
+ expect(job.status).toBe("pending");
21
+ expect(job.isPending()).toBe(true);
22
+ expect(job.isRunning()).toBe(false);
23
+ expect(job.isSuccess()).toBe(false);
24
+ expect(job.isFailed()).toBe(false);
25
+ expect(job.result).toBeNull();
26
+ expect(job.error).toBeNull();
27
+ expect(job.endedAt).toBeNull();
28
+ });
29
+
30
+ it("startするとrunningになる", () => {
31
+ const job = new AppsScriptJob(
32
+ "test",
33
+ async () => "result",
34
+ vi.fn(),
35
+ vi.fn(),
36
+ );
37
+
38
+ job.start();
39
+
40
+ expect(job.status).toBe("running");
41
+ expect(job.isRunning()).toBe(true);
42
+ });
43
+
44
+ it("successするとresultを保持してPromiseをresolveする", () => {
45
+ const resolve = vi.fn();
46
+
47
+ const job = new AppsScriptJob(
48
+ "test",
49
+ async () => "result",
50
+ resolve,
51
+ vi.fn(),
52
+ );
53
+
54
+ job.start();
55
+ job.success("success");
56
+
57
+ expect(job.status).toBe("success");
58
+ expect(job.result).toBe("success");
59
+ expect(job.error).toBeNull();
60
+ expect(job.endedAt).toBeInstanceOf(Date);
61
+ expect(resolve).toHaveBeenCalledWith("success");
62
+ });
63
+
64
+ it("failするとerrorを保持してPromiseをrejectする", () => {
65
+ const reject = vi.fn();
66
+ const error = new Error("failed");
67
+
68
+ const job = new AppsScriptJob(
69
+ "test",
70
+ async () => "result",
71
+ vi.fn(),
72
+ reject,
73
+ );
74
+
75
+ job.start();
76
+ job.fail(error);
77
+
78
+ expect(job.status).toBe("failed");
79
+ expect(job.error).toBe(error);
80
+ expect(job.endedAt).toBeInstanceOf(Date);
81
+ expect(reject).toHaveBeenCalledWith(error);
82
+ });
83
+ });
@@ -0,0 +1,125 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { AppsScriptJob } from "../src/job/AppsScriptJob";
3
+ import {
4
+ AppsScriptJobQueue,
5
+ type RunnableJob,
6
+ } from "../src/job/AppsScriptJobQueue";
7
+
8
+ describe("AppsScriptJobQueue", () => {
9
+ it("enqueueするとlistenerにJobを通知してrunする", () => {
10
+ const queue = new AppsScriptJobQueue();
11
+
12
+ const listener: RunnableJob = {
13
+ addJob: vi.fn(),
14
+ run: vi.fn(async () => {}),
15
+ };
16
+
17
+ queue.subscribe(listener);
18
+
19
+ void queue.enqueue("test", async () => "result");
20
+
21
+ expect(listener.addJob).toHaveBeenCalledTimes(1);
22
+ expect(listener.run).toHaveBeenCalledTimes(1);
23
+
24
+ const job = vi.mocked(listener.addJob).mock.calls[0][0];
25
+
26
+ expect(job.label).toBe("test");
27
+ expect(job.status).toBe("pending");
28
+ });
29
+
30
+ it("FIFOでdequeueする", () => {
31
+ const queue = new AppsScriptJobQueue();
32
+
33
+ void queue.enqueue("first", async () => 1);
34
+ void queue.enqueue("second", async () => 2);
35
+ void queue.enqueue("third", async () => 3);
36
+
37
+ expect(queue.dequeue()?.label).toBe("first");
38
+ expect(queue.dequeue()?.label).toBe("second");
39
+ expect(queue.dequeue()?.label).toBe("third");
40
+ });
41
+
42
+ it("空のQueueからdequeueするとundefined", () => {
43
+ const queue = new AppsScriptJobQueue();
44
+
45
+ expect(queue.dequeue()).toBeUndefined();
46
+ });
47
+
48
+ it("複数subscriberすべてに通知する", () => {
49
+ const queue = new AppsScriptJobQueue();
50
+
51
+ const first: RunnableJob = {
52
+ addJob: vi.fn(),
53
+ run: vi.fn(async () => {}),
54
+ };
55
+
56
+ const second: RunnableJob = {
57
+ addJob: vi.fn(),
58
+ run: vi.fn(async () => {}),
59
+ };
60
+
61
+ queue.subscribe(first);
62
+ queue.subscribe(second);
63
+
64
+ void queue.enqueue("test", async () => "result");
65
+
66
+ expect(first.addJob).toHaveBeenCalledTimes(1);
67
+ expect(second.addJob).toHaveBeenCalledTimes(1);
68
+ });
69
+
70
+ it("unsubscribeすると通知されない", () => {
71
+ const queue = new AppsScriptJobQueue();
72
+
73
+ const listener: RunnableJob = {
74
+ addJob: vi.fn(),
75
+ run: vi.fn(async () => {}),
76
+ };
77
+
78
+ const unsubscribe = queue.subscribe(listener);
79
+
80
+ unsubscribe();
81
+
82
+ void queue.enqueue("test", async () => "result");
83
+
84
+ expect(listener.addJob).not.toHaveBeenCalled();
85
+ expect(listener.run).not.toHaveBeenCalled();
86
+ });
87
+
88
+ it("removeすると指定したJobをQueueから削除する", async () => {
89
+ const queue = new AppsScriptJobQueue();
90
+
91
+ const jobs: AppsScriptJob<any>[] = [];
92
+
93
+ queue.subscribe({
94
+ addJob(job) {
95
+ jobs.push(job);
96
+ },
97
+ run: vi.fn(async () => {}),
98
+ });
99
+
100
+ queue.enqueue("first", async () => 1);
101
+ queue.enqueue("second", async () => 2);
102
+ queue.enqueue("third", async () => 3);
103
+
104
+ const second = jobs.find((job) => job.label === "second");
105
+
106
+ expect(second).toBeDefined();
107
+
108
+ queue.remove(second!.id);
109
+ expect(queue.dequeue()?.label).toBe("first");
110
+ expect(queue.dequeue()?.label).toBe("third");
111
+ expect(queue.dequeue()).toBeUndefined();
112
+ });
113
+
114
+ it("存在しないJobをremoveしてもQueueを変更しない", () => {
115
+ const queue = new AppsScriptJobQueue();
116
+
117
+ void queue.enqueue("first", async () => 1);
118
+ void queue.enqueue("second", async () => 2);
119
+
120
+ queue.remove("missing");
121
+
122
+ expect(queue.dequeue()?.label).toBe("first");
123
+ expect(queue.dequeue()?.label).toBe("second");
124
+ });
125
+ });