browser-commander 0.18.0 → 0.19.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.
@@ -338,6 +338,137 @@ describe('download manager (issue #88)', () => {
338
338
  assert.match(cancelled[0].failure, /canceled/);
339
339
  });
340
340
 
341
+ it('should wait for staged bytes that land after the completion event', async () => {
342
+ // Issue #92: Chromium writes `<guid>.crdownload` and renames it, so the
343
+ // completion event can arrive before `<guid>` is openable. Publishing on
344
+ // the event alone turned a download that succeeded into
345
+ // `ENOENT ... .browser-commander-staging/<guid>`.
346
+ const session = createFakeCdpSession();
347
+ manager = await cdpManagerOver(session);
348
+ const completed = collect(DOWNLOAD_EVENT.COMPLETED);
349
+ const failed = collect(DOWNLOAD_EVENT.FAILED);
350
+
351
+ const guid = 'guid-0010';
352
+ const stagedPath = path.join(root, STAGING_DIRECTORY, guid);
353
+ session.emit('Browser.downloadWillBegin', {
354
+ guid,
355
+ url: 'https://example.com/late.pdf',
356
+ suggestedFilename: 'late.pdf',
357
+ });
358
+ // The event first, the bytes afterwards - the ordering from the report.
359
+ session.emit('Browser.downloadProgress', { guid, state: 'completed' });
360
+ await yieldToEngine();
361
+ assert.deepStrictEqual(completed, [], 'published before the bytes exist');
362
+ await fs.writeFile(stagedPath, 'late body');
363
+
364
+ await manager.idle();
365
+
366
+ assert.deepStrictEqual(failed, []);
367
+ assert.strictEqual(completed.length, 1);
368
+ assert.strictEqual(
369
+ await fs.readFile(completed[0].path, 'utf8'),
370
+ 'late body'
371
+ );
372
+ assert.deepStrictEqual(
373
+ await fs.readdir(path.join(root, STAGING_DIRECTORY)),
374
+ []
375
+ );
376
+ });
377
+
378
+ it('should not claim bytes while the partial file is still being written', async () => {
379
+ const session = createFakeCdpSession();
380
+ manager = await cdpManagerOver(session);
381
+ const completed = collect(DOWNLOAD_EVENT.COMPLETED);
382
+
383
+ const guid = 'guid-0011';
384
+ const stagedPath = path.join(root, STAGING_DIRECTORY, guid);
385
+ session.emit('Browser.downloadWillBegin', {
386
+ guid,
387
+ url: 'https://example.com/partial.pdf',
388
+ suggestedFilename: 'partial.pdf',
389
+ });
390
+ // A half-written file under the final name, with Chromium's own
391
+ // in-progress marker still beside it.
392
+ await fs.writeFile(stagedPath, 'half');
393
+ await fs.writeFile(`${stagedPath}.crdownload`, 'rest');
394
+ session.emit('Browser.downloadProgress', { guid, state: 'completed' });
395
+ await yieldToEngine();
396
+ await yieldToEngine();
397
+ assert.deepStrictEqual(completed, [], 'claimed a half-written file');
398
+
399
+ await fs.rm(`${stagedPath}.crdownload`);
400
+ await fs.writeFile(stagedPath, 'half and rest');
401
+ await manager.idle();
402
+
403
+ assert.strictEqual(completed.length, 1);
404
+ assert.strictEqual(
405
+ await fs.readFile(completed[0].path, 'utf8'),
406
+ 'half and rest'
407
+ );
408
+ });
409
+
410
+ it('should report a completion whose bytes never arrive as a failure', async () => {
411
+ const session = createFakeCdpSession();
412
+ manager = await createDownloadManager({
413
+ engine: 'puppeteer',
414
+ directory: root,
415
+ browser: {
416
+ target: () => ({ createCDPSession: async () => session }),
417
+ },
418
+ stagingTimeout: 60,
419
+ stagingPollInterval: 5,
420
+ });
421
+ const failed = collect(DOWNLOAD_EVENT.FAILED);
422
+ const completed = collect(DOWNLOAD_EVENT.COMPLETED);
423
+
424
+ session.emit('Browser.downloadWillBegin', {
425
+ guid: 'guid-0012',
426
+ url: 'https://example.com/ghost.pdf',
427
+ suggestedFilename: 'ghost.pdf',
428
+ });
429
+ session.emit('Browser.downloadProgress', {
430
+ guid: 'guid-0012',
431
+ state: 'completed',
432
+ });
433
+ await manager.idle();
434
+
435
+ assert.deepStrictEqual(completed, []);
436
+ assert.strictEqual(failed.length, 1);
437
+ assert.match(failed[0].failure, /was not readable within 60ms/);
438
+ assert.match(failed[0].failure, /guid-0012/);
439
+ });
440
+
441
+ it('should keep a capture waiting until the staged bytes are readable', async () => {
442
+ const session = createFakeCdpSession();
443
+ manager = await cdpManagerOver(session);
444
+
445
+ const guid = 'guid-0013';
446
+ const stagedPath = path.join(root, STAGING_DIRECTORY, guid);
447
+ const artifact = await manager.capture({
448
+ timeout: 5000,
449
+ action: async () => {
450
+ session.emit('Browser.downloadWillBegin', {
451
+ guid,
452
+ url: 'https://example.com/captured.pdf',
453
+ suggestedFilename: 'captured.pdf',
454
+ });
455
+ session.emit('Browser.downloadProgress', {
456
+ guid,
457
+ state: 'completed',
458
+ });
459
+ setTimeout(() => {
460
+ fs.writeFile(stagedPath, 'captured body').catch(() => {});
461
+ }, 40);
462
+ },
463
+ });
464
+
465
+ assert.strictEqual(artifact.state, DOWNLOAD_EVENT.COMPLETED);
466
+ assert.strictEqual(
467
+ await fs.readFile(artifact.path, 'utf8'),
468
+ 'captured body'
469
+ );
470
+ });
471
+
341
472
  it('should ignore progress for a download it never saw start', async () => {
342
473
  const session = createFakeCdpSession();
343
474
  manager = await cdpManagerOver(session);
@@ -0,0 +1,117 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ import {
7
+ DEFAULT_STAGING_POLL_INTERVAL,
8
+ DEFAULT_STAGING_TIMEOUT,
9
+ describeStagingTimeout,
10
+ STAGING_IN_PROGRESS_SUFFIXES,
11
+ waitForStagedFile,
12
+ } from '../../../src/downloads/staging.js';
13
+ import { useTempDownloadDirectory } from '../../helpers/download-fixtures.js';
14
+
15
+ describe('waiting for staged bytes (issue #92)', () => {
16
+ const directory = useTempDownloadDirectory();
17
+
18
+ it('should report a file that is already complete', async () => {
19
+ const staged = path.join(directory.path, 'guid-ready');
20
+ await fs.writeFile(staged, 'complete body');
21
+
22
+ const settled = await waitForStagedFile({ path: staged, timeout: 1000 });
23
+
24
+ assert.strictEqual(settled.ready, true);
25
+ assert.strictEqual(settled.bytes, 'complete body'.length);
26
+ assert.strictEqual(settled.reason, null);
27
+ });
28
+
29
+ it('should wait for a file that appears after the completion event', async () => {
30
+ // This is issue #92 exactly: the browser says "completed" and the rename
31
+ // Chromium does behind it has not become visible yet.
32
+ const staged = path.join(directory.path, 'guid-late');
33
+ const appear = setTimeout(() => {
34
+ fs.writeFile(staged, 'late body').catch(() => {});
35
+ }, 60);
36
+
37
+ const settled = await waitForStagedFile({
38
+ path: staged,
39
+ timeout: 5000,
40
+ interval: 5,
41
+ });
42
+ clearTimeout(appear);
43
+
44
+ assert.strictEqual(settled.ready, true, settled.reason ?? '');
45
+ assert.strictEqual(settled.bytes, 'late body'.length);
46
+ });
47
+
48
+ it('should keep waiting while the file is still growing', async () => {
49
+ const staged = path.join(directory.path, 'guid-growing');
50
+ await fs.writeFile(staged, 'a');
51
+ const grow = setInterval(() => {
52
+ fs.appendFile(staged, 'a').catch(() => {});
53
+ }, 5);
54
+ setTimeout(() => clearInterval(grow), 80);
55
+
56
+ const settled = await waitForStagedFile({
57
+ path: staged,
58
+ timeout: 5000,
59
+ interval: 5,
60
+ });
61
+ clearInterval(grow);
62
+
63
+ assert.strictEqual(settled.ready, true, settled.reason ?? '');
64
+ // Two equal readings are what ends the wait, so the file had stopped
65
+ // growing before it was declared ready.
66
+ assert.strictEqual(settled.bytes, (await fs.stat(staged)).size);
67
+ });
68
+
69
+ it('should not claim a file whose partial sibling is still being written', async () => {
70
+ const staged = path.join(directory.path, 'guid-partial');
71
+ await fs.writeFile(staged, 'looks done');
72
+ await fs.writeFile(`${staged}.crdownload`, 'still writing');
73
+
74
+ const settled = await waitForStagedFile({
75
+ path: staged,
76
+ timeout: 60,
77
+ interval: 5,
78
+ });
79
+
80
+ assert.strictEqual(settled.ready, false);
81
+ assert.match(settled.reason, /\.crdownload file is still being written/);
82
+ });
83
+
84
+ it('should fail explicitly when the bytes never arrive', async () => {
85
+ const staged = path.join(directory.path, 'guid-missing');
86
+
87
+ const settled = await waitForStagedFile({
88
+ path: staged,
89
+ timeout: 60,
90
+ interval: 5,
91
+ });
92
+
93
+ assert.strictEqual(settled.ready, false);
94
+ assert.strictEqual(settled.bytes, null);
95
+ assert.match(settled.reason, /has not appeared/);
96
+ });
97
+
98
+ it('should name the file, the budget and what it last saw', () => {
99
+ const message = describeStagingTimeout({
100
+ path: '/tmp/root/.browser-commander-staging/guid',
101
+ timeout: 250,
102
+ reason: 'the file has not appeared yet',
103
+ });
104
+
105
+ assert.match(message, /reported the download as completed/);
106
+ assert.match(message, /guid/);
107
+ assert.match(message, /250ms/);
108
+ assert.match(message, /has not appeared yet/);
109
+ });
110
+
111
+ it('should ship defaults a download can actually finish within', () => {
112
+ assert.ok(DEFAULT_STAGING_TIMEOUT >= 1000);
113
+ assert.ok(DEFAULT_STAGING_POLL_INTERVAL > 0);
114
+ assert.ok(DEFAULT_STAGING_POLL_INTERVAL < DEFAULT_STAGING_TIMEOUT);
115
+ assert.ok(STAGING_IN_PROGRESS_SUFFIXES.includes('.crdownload'));
116
+ });
117
+ });