|
|
|
@ -14,9 +14,9 @@ export interface TestFile {
|
|
|
|
export function createTestFile(fileName: string, content: string): TestFile {
|
|
|
|
export function createTestFile(fileName: string, content: string): TestFile {
|
|
|
|
const testFilePath = join(tmpdir(), fileName)
|
|
|
|
const testFilePath = join(tmpdir(), fileName)
|
|
|
|
writeFileSync(testFilePath, content)
|
|
|
|
writeFileSync(testFilePath, content)
|
|
|
|
|
|
|
|
|
|
|
|
const checksum = createHash('sha256').update(content).digest('hex')
|
|
|
|
const checksum = createHash('sha256').update(content).digest('hex')
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
name: fileName,
|
|
|
|
name: fileName,
|
|
|
|
content,
|
|
|
|
content,
|
|
|
|
@ -25,7 +25,10 @@ export function createTestFile(fileName: string, content: string): TestFile {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function uploadFile(page: Page, testFile: TestFile): Promise<void> {
|
|
|
|
export async function uploadFile(
|
|
|
|
|
|
|
|
page: Page,
|
|
|
|
|
|
|
|
testFile: TestFile,
|
|
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
// Navigate to home page
|
|
|
|
// Navigate to home page
|
|
|
|
await page.goto('http://localhost:3000/')
|
|
|
|
await page.goto('http://localhost:3000/')
|
|
|
|
await expect(
|
|
|
|
await expect(
|
|
|
|
@ -58,9 +61,9 @@ export async function uploadFile(page: Page, testFile: TestFile): Promise<void>
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Wait for file to be processed and confirm upload page to appear
|
|
|
|
// Wait for file to be processed and confirm upload page to appear
|
|
|
|
await expect(
|
|
|
|
await expect(page.getByText(/You are about to start uploading/i)).toBeVisible(
|
|
|
|
page.getByText(/You are about to start uploading/i),
|
|
|
|
{ timeout: 10000 },
|
|
|
|
).toBeVisible({ timeout: 10000 })
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function startUpload(page: Page): Promise<string> {
|
|
|
|
export async function startUpload(page: Page): Promise<string> {
|
|
|
|
@ -81,7 +84,11 @@ export async function startUpload(page: Page): Promise<string> {
|
|
|
|
return shareUrl
|
|
|
|
return shareUrl
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function downloadFile(page: Page, shareUrl: string, testFile: TestFile): Promise<string> {
|
|
|
|
export async function downloadFile(
|
|
|
|
|
|
|
|
page: Page,
|
|
|
|
|
|
|
|
shareUrl: string,
|
|
|
|
|
|
|
|
testFile: TestFile,
|
|
|
|
|
|
|
|
): Promise<string> {
|
|
|
|
// Navigate to share URL
|
|
|
|
// Navigate to share URL
|
|
|
|
await page.goto(shareUrl)
|
|
|
|
await page.goto(shareUrl)
|
|
|
|
|
|
|
|
|
|
|
|
@ -105,7 +112,10 @@ export async function downloadFile(page: Page, shareUrl: string, testFile: TestF
|
|
|
|
return downloadPath
|
|
|
|
return downloadPath
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function verifyFileIntegrity(downloadPath: string, testFile: TestFile): Promise<void> {
|
|
|
|
export async function verifyFileIntegrity(
|
|
|
|
|
|
|
|
downloadPath: string,
|
|
|
|
|
|
|
|
testFile: TestFile,
|
|
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
// Verify downloaded content and checksum
|
|
|
|
// Verify downloaded content and checksum
|
|
|
|
const downloadedContent = readFileSync(downloadPath, 'utf8')
|
|
|
|
const downloadedContent = readFileSync(downloadPath, 'utf8')
|
|
|
|
expect(downloadedContent).toBe(testFile.content)
|
|
|
|
expect(downloadedContent).toBe(testFile.content)
|
|
|
|
@ -116,7 +126,9 @@ export async function verifyFileIntegrity(downloadPath: string, testFile: TestFi
|
|
|
|
expect(downloadedChecksum).toBe(testFile.checksum)
|
|
|
|
expect(downloadedChecksum).toBe(testFile.checksum)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function verifyTransferCompletion(downloaderPage: Page): Promise<void> {
|
|
|
|
export async function verifyTransferCompletion(
|
|
|
|
|
|
|
|
downloaderPage: Page,
|
|
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
// Verify download completion on downloader side
|
|
|
|
// Verify download completion on downloader side
|
|
|
|
await expect(downloaderPage.getByText(/You downloaded/i)).toBeVisible({
|
|
|
|
await expect(downloaderPage.getByText(/You downloaded/i)).toBeVisible({
|
|
|
|
timeout: 10000,
|
|
|
|
timeout: 10000,
|
|
|
|
@ -171,104 +183,68 @@ export async function monitorChunkProgress(
|
|
|
|
): Promise<PreciseChunkMonitor> {
|
|
|
|
): Promise<PreciseChunkMonitor> {
|
|
|
|
const uploadChunks: ChunkProgressLog[] = []
|
|
|
|
const uploadChunks: ChunkProgressLog[] = []
|
|
|
|
const downloadChunks: ChunkProgressLog[] = []
|
|
|
|
const downloadChunks: ChunkProgressLog[] = []
|
|
|
|
|
|
|
|
|
|
|
|
uploaderPage.on('console', async (msg) => {
|
|
|
|
uploaderPage.on('console', async (msg) => {
|
|
|
|
const text = msg.text()
|
|
|
|
const text = msg.text()
|
|
|
|
if (text.includes('[UploaderConnections] received chunk ack')) {
|
|
|
|
if (text.includes('[UploaderConnections] received chunk ack')) {
|
|
|
|
// Parse ack log: "[UploaderConnections] received chunk ack: file.txt offset 0 bytes 262144"
|
|
|
|
// Parse ack log: "[UploaderConnections] received chunk ack: file.txt offset 0 bytes 262144"
|
|
|
|
const ackMatch = text.match(/received chunk ack: (\S+) offset (\d+) bytes (\d+)/)
|
|
|
|
const ackMatch = text.match(
|
|
|
|
|
|
|
|
/received chunk ack: (\S+) offset (\d+) bytes (\d+)/,
|
|
|
|
|
|
|
|
)
|
|
|
|
if (ackMatch) {
|
|
|
|
if (ackMatch) {
|
|
|
|
const [, fileName, offset, bytes] = ackMatch
|
|
|
|
const [, fileName, offset, bytes] = ackMatch
|
|
|
|
|
|
|
|
|
|
|
|
// Wait for React state to update, then capture progress percentage
|
|
|
|
// Calculate which chunk this corresponds to and expected progress
|
|
|
|
setTimeout(async () => {
|
|
|
|
const chunkNumber = Math.floor(parseInt(offset) / (256 * 1024)) + 1
|
|
|
|
try {
|
|
|
|
const chunkEnd = parseInt(offset) + parseInt(bytes)
|
|
|
|
// Debug: check all progress elements
|
|
|
|
const final = chunkEnd >= expectedFileSize
|
|
|
|
const allProgressElements = await uploaderPage.locator('#progress-percentage').all()
|
|
|
|
const progressPercentage = Math.round(
|
|
|
|
console.log(`Found ${allProgressElements.length} progress elements on uploader page`)
|
|
|
|
(chunkEnd / expectedFileSize) * 100,
|
|
|
|
|
|
|
|
)
|
|
|
|
const progressElement = uploaderPage.locator('#progress-percentage').first()
|
|
|
|
|
|
|
|
const progressText = await progressElement.textContent({ timeout: 200 })
|
|
|
|
uploadChunks.push({
|
|
|
|
const progressPercentage = progressText ? parseInt(progressText.replace('%', '')) : 0
|
|
|
|
chunkNumber,
|
|
|
|
|
|
|
|
fileName,
|
|
|
|
console.log(`Uploader progress text: "${progressText}" -> ${progressPercentage}%`)
|
|
|
|
offset: parseInt(offset),
|
|
|
|
|
|
|
|
end: chunkEnd,
|
|
|
|
// Calculate which chunk this corresponds to
|
|
|
|
fileSize: expectedFileSize,
|
|
|
|
const chunkNumber = Math.floor(parseInt(offset) / (256 * 1024)) + 1
|
|
|
|
final,
|
|
|
|
const chunkEnd = parseInt(offset) + parseInt(bytes)
|
|
|
|
progressPercentage,
|
|
|
|
const final = chunkEnd >= expectedFileSize
|
|
|
|
side: 'upload',
|
|
|
|
|
|
|
|
})
|
|
|
|
uploadChunks.push({
|
|
|
|
|
|
|
|
chunkNumber,
|
|
|
|
|
|
|
|
fileName,
|
|
|
|
|
|
|
|
offset: parseInt(offset),
|
|
|
|
|
|
|
|
end: chunkEnd,
|
|
|
|
|
|
|
|
fileSize: expectedFileSize,
|
|
|
|
|
|
|
|
final,
|
|
|
|
|
|
|
|
progressPercentage,
|
|
|
|
|
|
|
|
side: 'upload',
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
// Progress element might not be available yet
|
|
|
|
|
|
|
|
const chunkNumber = Math.floor(parseInt(offset) / (256 * 1024)) + 1
|
|
|
|
|
|
|
|
const chunkEnd = parseInt(offset) + parseInt(bytes)
|
|
|
|
|
|
|
|
const final = chunkEnd >= expectedFileSize
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
uploadChunks.push({
|
|
|
|
|
|
|
|
chunkNumber,
|
|
|
|
|
|
|
|
fileName,
|
|
|
|
|
|
|
|
offset: parseInt(offset),
|
|
|
|
|
|
|
|
end: chunkEnd,
|
|
|
|
|
|
|
|
fileSize: expectedFileSize,
|
|
|
|
|
|
|
|
final,
|
|
|
|
|
|
|
|
progressPercentage: 0,
|
|
|
|
|
|
|
|
side: 'upload',
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}, 100) // Slightly longer delay for ack processing
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
downloaderPage.on('console', async (msg) => {
|
|
|
|
downloaderPage.on('console', async (msg) => {
|
|
|
|
const text = msg.text()
|
|
|
|
const text = msg.text()
|
|
|
|
if (text.includes('[Downloader] received chunk') && !text.includes('finished receiving')) {
|
|
|
|
if (
|
|
|
|
|
|
|
|
text.includes('[Downloader] received chunk') &&
|
|
|
|
|
|
|
|
!text.includes('finished receiving')
|
|
|
|
|
|
|
|
) {
|
|
|
|
// Parse log: "[Downloader] received chunk 1 for file.txt (0-262144) final=false"
|
|
|
|
// Parse log: "[Downloader] received chunk 1 for file.txt (0-262144) final=false"
|
|
|
|
const chunkMatch = text.match(/received chunk (\d+) for (\S+) \((\d+)-(\d+)\) final=(\w+)/)
|
|
|
|
const chunkMatch = text.match(
|
|
|
|
|
|
|
|
/received chunk (\d+) for (\S+) \((\d+)-(\d+)\) final=(\w+)/,
|
|
|
|
|
|
|
|
)
|
|
|
|
if (chunkMatch) {
|
|
|
|
if (chunkMatch) {
|
|
|
|
const [, chunkNum, fileName, offset, end, final] = chunkMatch
|
|
|
|
const [, chunkNum, fileName, offset, end, final] = chunkMatch
|
|
|
|
|
|
|
|
|
|
|
|
// Wait a moment for React state to update, then capture progress percentage
|
|
|
|
// Calculate expected progress based on chunk data
|
|
|
|
setTimeout(async () => {
|
|
|
|
const chunkEnd = parseInt(end)
|
|
|
|
try {
|
|
|
|
const progressPercentage = Math.round(
|
|
|
|
const progressElement = downloaderPage.locator('#progress-percentage').first()
|
|
|
|
(chunkEnd / expectedFileSize) * 100,
|
|
|
|
const progressText = await progressElement.textContent({ timeout: 200 })
|
|
|
|
)
|
|
|
|
const progressPercentage = progressText ? parseInt(progressText.replace('%', '')) : 0
|
|
|
|
|
|
|
|
|
|
|
|
downloadChunks.push({
|
|
|
|
downloadChunks.push({
|
|
|
|
chunkNumber: parseInt(chunkNum),
|
|
|
|
chunkNumber: parseInt(chunkNum),
|
|
|
|
fileName,
|
|
|
|
fileName,
|
|
|
|
offset: parseInt(offset),
|
|
|
|
offset: parseInt(offset),
|
|
|
|
end: chunkEnd,
|
|
|
|
end: parseInt(end),
|
|
|
|
fileSize: expectedFileSize,
|
|
|
|
fileSize: expectedFileSize,
|
|
|
|
final: final === 'true',
|
|
|
|
final: final === 'true',
|
|
|
|
progressPercentage,
|
|
|
|
progressPercentage,
|
|
|
|
side: 'download',
|
|
|
|
side: 'download',
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
// Progress element might not be available yet
|
|
|
|
|
|
|
|
downloadChunks.push({
|
|
|
|
|
|
|
|
chunkNumber: parseInt(chunkNum),
|
|
|
|
|
|
|
|
fileName,
|
|
|
|
|
|
|
|
offset: parseInt(offset),
|
|
|
|
|
|
|
|
end: parseInt(end),
|
|
|
|
|
|
|
|
fileSize: expectedFileSize,
|
|
|
|
|
|
|
|
final: final === 'true',
|
|
|
|
|
|
|
|
progressPercentage: 0,
|
|
|
|
|
|
|
|
side: 'download',
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}, 50) // Small delay to allow React state update
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
@ -280,82 +256,38 @@ export async function monitorChunkProgress(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function verifyPreciseProgress(
|
|
|
|
export function verifyPreciseProgress(
|
|
|
|
chunks: ChunkProgressLog[],
|
|
|
|
chunks: ChunkProgressLog[],
|
|
|
|
expectedChunks: number,
|
|
|
|
expectedChunks: number,
|
|
|
|
side: 'upload' | 'download'
|
|
|
|
side: 'upload' | 'download',
|
|
|
|
): void {
|
|
|
|
): void {
|
|
|
|
expect(chunks.length).toBe(expectedChunks)
|
|
|
|
expect(chunks.length).toBe(expectedChunks)
|
|
|
|
|
|
|
|
|
|
|
|
for (const chunk of chunks) {
|
|
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
|
|
// Calculate expected progress percentage for this chunk
|
|
|
|
const chunk = chunks[i]
|
|
|
|
const expectedProgress = Math.round((chunk.end / chunk.fileSize) * 100)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
console.log(
|
|
|
|
`${side} chunk ${chunk.chunkNumber}: ${chunk.offset}-${chunk.end}/${chunk.fileSize} ` +
|
|
|
|
`${side} chunk ${chunk.chunkNumber}: ${chunk.offset}-${chunk.end}/${chunk.fileSize} ` +
|
|
|
|
`expected=${expectedProgress}% actual=${chunk.progressPercentage}% final=${chunk.final}`
|
|
|
|
`progress=${chunk.progressPercentage}% final=${chunk.final}`,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// For the final chunk, ensure we reach exactly 100%
|
|
|
|
|
|
|
|
if (chunk.final) {
|
|
|
|
|
|
|
|
expect(chunk.progressPercentage).toBe(100)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// For non-final chunks, allow small tolerance due to rounding and UI update timing
|
|
|
|
|
|
|
|
expect(chunk.progressPercentage).toBeGreaterThanOrEqual(expectedProgress - 2)
|
|
|
|
|
|
|
|
expect(chunk.progressPercentage).toBeLessThanOrEqual(expectedProgress + 2)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function monitorTransferProgress(
|
|
|
|
// Verify chunks are received in order
|
|
|
|
uploaderPage: Page,
|
|
|
|
expect(chunk.chunkNumber).toBe(i + 1)
|
|
|
|
downloaderPage: Page,
|
|
|
|
|
|
|
|
maxChecks: number = 10,
|
|
|
|
|
|
|
|
): Promise<ProgressMonitor> {
|
|
|
|
|
|
|
|
let uploaderProgress = -1
|
|
|
|
|
|
|
|
let downloaderProgress = -1
|
|
|
|
|
|
|
|
let maxProgress = 0
|
|
|
|
|
|
|
|
let progressChecks = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Wait a moment for transfer to start
|
|
|
|
|
|
|
|
await downloaderPage.waitForTimeout(500)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check that progress bars appear on both sides
|
|
|
|
|
|
|
|
await expect(downloaderPage.locator('#progress-bar')).toBeVisible({ timeout: 5000 })
|
|
|
|
|
|
|
|
await expect(uploaderPage.locator('#progress-bar')).toBeVisible({ timeout: 5000 })
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
while (progressChecks < maxChecks) {
|
|
|
|
|
|
|
|
// Check downloader progress
|
|
|
|
|
|
|
|
const downloaderProgressText = await downloaderPage.locator('#progress-percentage').textContent()
|
|
|
|
|
|
|
|
if (downloaderProgressText) {
|
|
|
|
|
|
|
|
const newDownloaderProgress = parseInt(downloaderProgressText.replace('%', ''))
|
|
|
|
|
|
|
|
if (newDownloaderProgress > downloaderProgress) {
|
|
|
|
|
|
|
|
downloaderProgress = newDownloaderProgress
|
|
|
|
|
|
|
|
maxProgress = Math.max(maxProgress, newDownloaderProgress)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check uploader progress
|
|
|
|
// Verify progress is monotonically increasing
|
|
|
|
const uploaderProgressText = await uploaderPage.locator('#progress-percentage').textContent()
|
|
|
|
if (i > 0) {
|
|
|
|
if (uploaderProgressText) {
|
|
|
|
expect(chunk.progressPercentage).toBeGreaterThanOrEqual(
|
|
|
|
const newUploaderProgress = parseInt(uploaderProgressText.replace('%', ''))
|
|
|
|
chunks[i - 1].progressPercentage,
|
|
|
|
if (newUploaderProgress > uploaderProgress) {
|
|
|
|
)
|
|
|
|
uploaderProgress = newUploaderProgress
|
|
|
|
|
|
|
|
maxProgress = Math.max(maxProgress, newUploaderProgress)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Break if both have made significant progress or completed
|
|
|
|
// For the final chunk, ensure we reach exactly 100%
|
|
|
|
if (downloaderProgress >= 50 && uploaderProgress >= 50) {
|
|
|
|
if (chunk.final) {
|
|
|
|
break
|
|
|
|
expect(chunk.progressPercentage).toBe(100)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await downloaderPage.waitForTimeout(200)
|
|
|
|
// Verify progress percentage is reasonable (0-100%)
|
|
|
|
progressChecks++
|
|
|
|
expect(chunk.progressPercentage).toBeGreaterThanOrEqual(0)
|
|
|
|
|
|
|
|
expect(chunk.progressPercentage).toBeLessThanOrEqual(100)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
uploaderProgress,
|
|
|
|
|
|
|
|
downloaderProgress,
|
|
|
|
|
|
|
|
maxProgress,
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|