Skip to content

Commit 6d81908

Browse files
Address CodeRabbit review on downloads E2E helpers and specs.
Reuse shared download launch/state helpers in video_download, harden server teardown and popup cleanup, import IPC channel constants, and poll for dropdown item count in clear-all spec. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e81d4dd commit 6d81908

4 files changed

Lines changed: 77 additions & 125 deletions

File tree

e2e/helpers/downloads.ts

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import * as fs from 'fs';
55
import * as http from 'http';
6+
import type {Socket} from 'net';
67
import * as path from 'path';
78

89
import {expect} from '@playwright/test';
@@ -24,6 +25,7 @@ export async function startDownloadServer(
2425
): Promise<DownloadServer> {
2526
const contents = options?.contents ?? 'download test contents';
2627
const slow = options?.slow ?? false;
28+
const connections = new Set<Socket>();
2729

2830
const server = http.createServer((request, response) => {
2931
if (request.url === '/download.txt') {
@@ -93,6 +95,11 @@ export async function startDownloadServer(
9395
`);
9496
});
9597

98+
server.on('connection', (socket) => {
99+
connections.add(socket);
100+
socket.on('close', () => connections.delete(socket));
101+
});
102+
96103
await new Promise<void>((resolve, reject) => {
97104
server.once('error', reject);
98105
server.listen(0, '127.0.0.1', () => resolve());
@@ -106,6 +113,9 @@ export async function startDownloadServer(
106113
server,
107114
url: `http://127.0.0.1:${address.port}`,
108115
close: () => new Promise<void>((resolve, reject) => {
116+
for (const socket of connections) {
117+
socket.destroy();
118+
}
109119
server.close((error) => (error ? reject(error) : resolve()));
110120
}),
111121
};
@@ -164,6 +174,11 @@ export async function triggerDownloadFromPopup(app: ElectronApplication, popupUr
164174
});
165175

166176
await app.evaluate(async ({BrowserWindow}, url) => {
177+
const existing = (global as any).__e2eDownloadPopup;
178+
if (existing && !existing.isDestroyed?.()) {
179+
existing.close();
180+
}
181+
167182
const popup = new BrowserWindow({
168183
show: true,
169184
width: 900,
@@ -228,25 +243,30 @@ export async function closeDownloadTestApp(app: ElectronApplication, userDataDir
228243

229244
await closeElectronAppFast(app, userDataDir).catch(() => {});
230245

231-
if (!fs.existsSync(downloadLocation)) {
232-
return;
233-
}
234-
235-
const timeout = process.platform === 'win32' ? 10_000 : 2_000;
236-
const deadline = Date.now() + timeout;
237-
let lastError: unknown;
238-
while (Date.now() < deadline) {
239-
try {
240-
fs.rmSync(downloadLocation, {recursive: true, force: true, maxRetries: 3, retryDelay: 200});
246+
const removeDirWithRetry = async (dir: string) => {
247+
if (!fs.existsSync(dir)) {
241248
return;
242-
} catch (error) {
243-
lastError = error;
244-
const code = (error as NodeJS.ErrnoException).code;
245-
if (code !== 'EBUSY' && code !== 'EPERM' && code !== 'ENOTEMPTY') {
246-
throw error;
249+
}
250+
251+
const timeout = process.platform === 'win32' ? 10_000 : 2_000;
252+
const deadline = Date.now() + timeout;
253+
let lastError: unknown;
254+
while (Date.now() < deadline) {
255+
try {
256+
fs.rmSync(dir, {recursive: true, force: true, maxRetries: 3, retryDelay: 200});
257+
return;
258+
} catch (error) {
259+
lastError = error;
260+
const code = (error as NodeJS.ErrnoException).code;
261+
if (code !== 'EBUSY' && code !== 'EPERM' && code !== 'ENOTEMPTY') {
262+
throw error;
263+
}
264+
await new Promise((resolve) => setTimeout(resolve, 250));
247265
}
248-
await new Promise((resolve) => setTimeout(resolve, 250));
249266
}
250-
}
251-
throw lastError ?? new Error(`Failed to remove download directory: ${downloadLocation}`);
267+
throw lastError ?? new Error(`Failed to remove directory: ${dir}`);
268+
};
269+
270+
await removeDirWithRetry(downloadLocation);
271+
await removeDirWithRetry(userDataDir);
252272
}

e2e/helpers/downloadsDropdown.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@
33

44
import type {ElectronApplication} from 'playwright';
55

6-
const CLOSE_DOWNLOADS_DROPDOWN = 'close-downloads-dropdown';
7-
const CLOSE_DOWNLOADS_DROPDOWN_MENU = 'close-downloads-dropdown-menu';
6+
import {CLOSE_DOWNLOADS_DROPDOWN, CLOSE_DOWNLOADS_DROPDOWN_MENU} from '../../src/common/communication';
7+
8+
function isTransientNavigationError(message: string): boolean {
9+
return message.includes('Execution context was destroyed') ||
10+
message.includes('Target closed') ||
11+
message.includes('Protocol error');
12+
}
813

914
/**
1015
* Close the downloads dropdown WebContentsView if it is open.
@@ -21,7 +26,7 @@ export async function closeDownloadsDropdownIfOpen(app: ElectronApplication): Pr
2126
return;
2227
} catch (error) {
2328
const message = error instanceof Error ? error.message : String(error);
24-
if (!message.includes('Execution context was destroyed')) {
29+
if (!isTransientNavigationError(message)) {
2530
throw error;
2631
}
2732
await new Promise((resolve) => setTimeout(resolve, 100));

e2e/specs/downloads/download_clear_all.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ test(
5252
try {
5353
const {downloadsWindow} = await openDownloadsDropdown(app);
5454
await downloadsWindow.waitForSelector('.DownloadsDropdown__File', {timeout: 10_000});
55-
expect(await downloadsWindow.locator('.DownloadsDropdown__File').count()).toBe(2);
55+
await expect.poll(
56+
() => downloadsWindow.locator('.DownloadsDropdown__File').count(),
57+
{timeout: 10_000},
58+
).toBe(2);
5659

5760
await downloadsWindow.click('.DownloadsDropdown__clearAllButton');
5861

e2e/specs/downloads/video_download.test.ts

Lines changed: 27 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import * as http from 'http';
66
import * as path from 'path';
77

88
import {test, expect} from '../../fixtures/index';
9-
import {waitForAppReady} from '../../helpers/appReadiness';
10-
import {electronBinaryPath, appDir, emptyConfig} from '../../helpers/config';
11-
import {closeElectronAppFast} from '../../helpers/electronApp';
9+
import {
10+
closeDownloadTestApp,
11+
launchAppWithDownloadsDir,
12+
readDownloadsState,
13+
triggerDownloadFromPopup,
14+
} from '../../helpers/downloads';
1215

1316
// ── MM-T1538: Download a video ────────────────────────────────────────
1417
// Verifies a real end-to-end download path for a video MIME type:
@@ -17,19 +20,6 @@ import {closeElectronAppFast} from '../../helpers/electronApp';
1720
// 3. DownloadsManager (src/main/downloadsManager.ts) handles will-download
1821
// 4. We assert: the file lands on disk AND downloads.json records it
1922
// with state "completed"
20-
//
21-
// Pattern mirrors download_completion.test.ts so the two tests differ only
22-
// in MIME type and content — keeping the download flow exercised for the
23-
// file type MM-T1538 specifically targets (video) without duplicating the
24-
// scaffolding.
25-
26-
function readJsonFile<T>(filePath: string): T | undefined {
27-
try {
28-
return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as T;
29-
} catch {
30-
return undefined;
31-
}
32-
}
3323

3424
async function startVideoServer(filename: string, body: Buffer) {
3525
const server = http.createServer((request, response) => {
@@ -54,17 +44,20 @@ async function startVideoServer(filename: string, body: Buffer) {
5444
`);
5545
});
5646

57-
await new Promise<void>((resolve) =>
58-
server.listen(0, '127.0.0.1', () => resolve()),
59-
);
47+
await new Promise<void>((resolve, reject) => {
48+
server.once('error', reject);
49+
server.listen(0, '127.0.0.1', () => resolve());
50+
});
6051
const address = server.address();
6152
if (!address || typeof address === 'string') {
6253
throw new Error('Failed to start local video download server');
6354
}
6455

6556
return {
66-
server,
6757
url: `http://127.0.0.1:${address.port}`,
58+
close: () => new Promise<void>((resolve, reject) => {
59+
server.close((error) => (error ? reject(error) : resolve()));
60+
}),
6861
};
6962
}
7063

@@ -74,100 +67,31 @@ test(
7467
async ({}, testInfo) => {
7568
const filename = 'sample-video.mp4';
7669

77-
// Minimal .mp4 — magic bytes are enough for the download-manager
78-
// pipeline; we never play the file, only assert it landed on disk.
7970
const videoBody = Buffer.from([
8071
0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70,
8172
0x6d, 0x70, 0x34, 0x32, 0x00, 0x00, 0x00, 0x00,
8273
0x6d, 0x70, 0x34, 0x32, 0x69, 0x73, 0x6f, 0x6d,
8374
]);
8475

85-
const {server, url} = await startVideoServer(filename, videoBody);
76+
const {url, close: closeVideoServer} = await startVideoServer(filename, videoBody);
8677

8778
const userDataDir = path.join(testInfo.outputDir, 'userdata');
88-
const downloadsDir = path.join(testInfo.outputDir, 'Downloads');
89-
const config = {
90-
...emptyConfig,
91-
downloadLocation: downloadsDir,
92-
};
93-
94-
fs.mkdirSync(userDataDir, {recursive: true});
95-
fs.mkdirSync(downloadsDir, {recursive: true});
96-
fs.writeFileSync(
97-
path.join(userDataDir, 'config.json'),
98-
JSON.stringify(config),
99-
);
100-
101-
const {_electron: electron} = await import('playwright');
102-
const app = await electron.launch({
103-
executablePath: electronBinaryPath,
104-
args: [
105-
appDir,
106-
`--user-data-dir=${userDataDir}`,
107-
'--no-sandbox',
108-
'--disable-gpu',
109-
],
110-
env: {...process.env, NODE_ENV: 'test'},
111-
timeout: 60_000,
112-
});
113-
114-
const savedPath = path.join(downloadsDir, filename);
79+
const downloadLocation = path.join(testInfo.outputDir, 'Downloads');
80+
const app = await launchAppWithDownloadsDir(userDataDir, downloadLocation);
81+
const savedPath = path.join(downloadLocation, filename);
11582

11683
try {
117-
await waitForAppReady(app);
118-
const mainWindow = app.windows().find((window) => window.url().includes('index'));
119-
expect(mainWindow).toBeDefined();
120-
await mainWindow!.waitForLoadState();
121-
122-
const popupPromise = app.waitForEvent('window', {
123-
predicate: (window) => window.url().startsWith(url),
124-
timeout: 15_000,
125-
});
126-
127-
await app.evaluate(async ({BrowserWindow}, popupUrl) => {
128-
const popup = new BrowserWindow({
129-
show: true,
130-
width: 900,
131-
height: 700,
132-
});
133-
await popup.loadURL(popupUrl);
134-
(global as any).__videoDownloadPopup = popup;
135-
}, url);
136-
137-
const popupWindow = await popupPromise;
138-
await popupWindow.waitForLoadState();
139-
await popupWindow.click('#download-link');
140-
141-
await expect.
142-
poll(() => fs.existsSync(savedPath), {timeout: 15_000}).
143-
toBe(true);
144-
145-
// Verify the downloaded bytes match what we served
146-
await expect.
147-
poll(() => fs.readFileSync(savedPath).equals(videoBody), {timeout: 15_000}).
148-
toBe(true);
149-
150-
// DownloadsManager must record the download as completed
151-
await expect.
152-
poll(
153-
() => {
154-
const downloads = readJsonFile<Record<string, {state?: string; mimeType?: string}>>(
155-
path.join(userDataDir, 'downloads.json'),
156-
);
157-
return downloads?.[filename]?.state;
158-
},
159-
{timeout: 15_000},
160-
).
161-
toBe('completed');
84+
await triggerDownloadFromPopup(app, url);
85+
86+
await expect.poll(() => fs.existsSync(savedPath), {timeout: 15_000}).toBe(true);
87+
await expect.poll(() => fs.readFileSync(savedPath).equals(videoBody), {timeout: 15_000}).toBe(true);
88+
await expect.poll(
89+
() => readDownloadsState(userDataDir)[filename]?.state,
90+
{timeout: 15_000},
91+
).toBe('completed');
16292
} finally {
163-
try {
164-
await closeElectronAppFast(app, userDataDir);
165-
} finally {
166-
await new Promise<void>((resolve, reject) =>
167-
server.close((error) => (error ? reject(error) : resolve())),
168-
);
169-
fs.rmSync(downloadsDir, {recursive: true, force: true});
170-
}
93+
await closeDownloadTestApp(app, userDataDir, downloadLocation);
94+
await closeVideoServer();
17195
}
17296
},
17397
);

0 commit comments

Comments
 (0)