mirror of https://github.com/kern/filepizza
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
294 lines
8.0 KiB
TypeScript
294 lines
8.0 KiB
TypeScript
import { Page, Browser, expect } from '@playwright/test'
|
|
import { createHash } from 'crypto'
|
|
import { writeFileSync, readFileSync } from 'fs'
|
|
import { join } from 'path'
|
|
import { tmpdir } from 'os'
|
|
|
|
export interface TestFile {
|
|
name: string
|
|
content: string
|
|
path: string
|
|
checksum: string
|
|
}
|
|
|
|
export function createTestFile(fileName: string, content: string): TestFile {
|
|
const testFilePath = join(tmpdir(), fileName)
|
|
writeFileSync(testFilePath, content)
|
|
|
|
const checksum = createHash('sha256').update(content).digest('hex')
|
|
|
|
return {
|
|
name: fileName,
|
|
content,
|
|
path: testFilePath,
|
|
checksum,
|
|
}
|
|
}
|
|
|
|
export async function uploadFile(
|
|
page: Page,
|
|
testFile: TestFile,
|
|
): Promise<void> {
|
|
// Navigate to home page
|
|
await page.goto('http://localhost:3000/')
|
|
await expect(
|
|
page.getByText('Peer-to-peer file transfers in your browser.'),
|
|
).toBeVisible()
|
|
|
|
// Wait for drop zone button to be ready
|
|
await expect(page.locator('#drop-zone-button')).toBeVisible()
|
|
|
|
// Upload file using the file input and trigger change event
|
|
await page.evaluate(
|
|
({ testContent, testFileName }) => {
|
|
const input = document.querySelector(
|
|
'input[type="file"]',
|
|
) as HTMLInputElement
|
|
if (input) {
|
|
const file = new File([testContent], testFileName, {
|
|
type: 'text/plain',
|
|
})
|
|
const dataTransfer = new DataTransfer()
|
|
dataTransfer.items.add(file)
|
|
input.files = dataTransfer.files
|
|
|
|
// Manually trigger the change event
|
|
const event = new Event('change', { bubbles: true })
|
|
input.dispatchEvent(event)
|
|
}
|
|
},
|
|
{ testContent: testFile.content, testFileName: testFile.name },
|
|
)
|
|
|
|
// Wait for file to be processed and confirm upload page to appear
|
|
await expect(page.getByText(/You are about to start uploading/i)).toBeVisible(
|
|
{ timeout: 10000 },
|
|
)
|
|
}
|
|
|
|
export async function startUpload(page: Page): Promise<string> {
|
|
// Start sharing
|
|
await page.locator('#start-button').click()
|
|
|
|
// Wait for uploading state and get the share URL
|
|
await expect(page.getByText(/You are uploading/i)).toBeVisible({
|
|
timeout: 10000,
|
|
})
|
|
|
|
// Get the share URL from the copyable input (Long URL)
|
|
const shareUrlInput = page.locator('#copyable-input-long-url')
|
|
await expect(shareUrlInput).toBeVisible({ timeout: 5000 })
|
|
const shareUrl = await shareUrlInput.inputValue()
|
|
|
|
expect(shareUrl).toMatch(/http:\/\/localhost:3000\//)
|
|
return shareUrl
|
|
}
|
|
|
|
export async function downloadFile(
|
|
page: Page,
|
|
shareUrl: string,
|
|
testFile: TestFile,
|
|
): Promise<string> {
|
|
// Navigate to share URL
|
|
await page.goto(shareUrl)
|
|
|
|
// Wait for download page to load
|
|
await expect(page.getByText(testFile.name)).toBeVisible({
|
|
timeout: 10000,
|
|
})
|
|
|
|
// Start download
|
|
const downloadPromise = page.waitForEvent('download')
|
|
await page.locator('#download-button').click()
|
|
const download = await downloadPromise
|
|
|
|
// Verify download
|
|
expect(download.suggestedFilename()).toBe(testFile.name)
|
|
|
|
// Save downloaded file
|
|
const downloadPath = join(tmpdir(), `downloaded-${testFile.name}`)
|
|
await download.saveAs(downloadPath)
|
|
|
|
return downloadPath
|
|
}
|
|
|
|
export async function verifyFileIntegrity(
|
|
downloadPath: string,
|
|
testFile: TestFile,
|
|
): Promise<void> {
|
|
// Verify downloaded content and checksum
|
|
const downloadedContent = readFileSync(downloadPath, 'utf8')
|
|
expect(downloadedContent).toBe(testFile.content)
|
|
|
|
const downloadedChecksum = createHash('sha256')
|
|
.update(downloadedContent)
|
|
.digest('hex')
|
|
expect(downloadedChecksum).toBe(testFile.checksum)
|
|
}
|
|
|
|
export async function verifyTransferCompletion(
|
|
downloaderPage: Page,
|
|
): Promise<void> {
|
|
// Verify download completion on downloader side
|
|
await expect(downloaderPage.getByText(/You downloaded/i)).toBeVisible({
|
|
timeout: 10000,
|
|
})
|
|
}
|
|
|
|
export async function createBrowserContexts(browser: Browser): Promise<{
|
|
uploaderPage: Page
|
|
downloaderPage: Page
|
|
cleanup: () => Promise<void>
|
|
}> {
|
|
const uploaderContext = await browser.newContext()
|
|
const downloaderContext = await browser.newContext()
|
|
|
|
const uploaderPage = await uploaderContext.newPage()
|
|
const downloaderPage = await downloaderContext.newPage()
|
|
|
|
const cleanup = async () => {
|
|
await uploaderContext.close()
|
|
await downloaderContext.close()
|
|
}
|
|
|
|
return { uploaderPage, downloaderPage, cleanup }
|
|
}
|
|
|
|
export interface ProgressMonitor {
|
|
uploaderProgress: number
|
|
downloaderProgress: number
|
|
maxProgress: number
|
|
}
|
|
|
|
export interface ChunkProgressLog {
|
|
chunkNumber: number
|
|
fileName: string
|
|
offset: number
|
|
end: number
|
|
fileSize: number
|
|
final: boolean
|
|
progressPercentage: number
|
|
side: 'upload' | 'download'
|
|
}
|
|
|
|
export interface PreciseChunkMonitor {
|
|
uploadChunks: ChunkProgressLog[]
|
|
downloadChunks: ChunkProgressLog[]
|
|
}
|
|
|
|
export async function monitorChunkProgress(
|
|
uploaderPage: Page,
|
|
downloaderPage: Page,
|
|
expectedFileSize: number,
|
|
): Promise<PreciseChunkMonitor> {
|
|
const uploadChunks: ChunkProgressLog[] = []
|
|
const downloadChunks: ChunkProgressLog[] = []
|
|
|
|
uploaderPage.on('console', async (msg) => {
|
|
const text = msg.text()
|
|
if (text.includes('[UploaderConnections] received chunk ack')) {
|
|
// 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+)/,
|
|
)
|
|
if (ackMatch) {
|
|
const [, fileName, offset, bytes] = ackMatch
|
|
|
|
// Calculate which chunk this corresponds to and expected progress
|
|
const chunkNumber = Math.floor(parseInt(offset) / (256 * 1024)) + 1
|
|
const chunkEnd = parseInt(offset) + parseInt(bytes)
|
|
const final = chunkEnd >= expectedFileSize
|
|
const progressPercentage = Math.round(
|
|
(chunkEnd / expectedFileSize) * 100,
|
|
)
|
|
|
|
uploadChunks.push({
|
|
chunkNumber,
|
|
fileName,
|
|
offset: parseInt(offset),
|
|
end: chunkEnd,
|
|
fileSize: expectedFileSize,
|
|
final,
|
|
progressPercentage,
|
|
side: 'upload',
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
downloaderPage.on('console', async (msg) => {
|
|
const text = msg.text()
|
|
if (
|
|
text.includes('[Downloader] received chunk') &&
|
|
!text.includes('finished receiving')
|
|
) {
|
|
// 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+)/,
|
|
)
|
|
if (chunkMatch) {
|
|
const [, chunkNum, fileName, offset, end, final] = chunkMatch
|
|
|
|
// Calculate expected progress based on chunk data
|
|
const chunkEnd = parseInt(end)
|
|
const progressPercentage = Math.round(
|
|
(chunkEnd / expectedFileSize) * 100,
|
|
)
|
|
|
|
downloadChunks.push({
|
|
chunkNumber: parseInt(chunkNum),
|
|
fileName,
|
|
offset: parseInt(offset),
|
|
end: chunkEnd,
|
|
fileSize: expectedFileSize,
|
|
final: final === 'true',
|
|
progressPercentage,
|
|
side: 'download',
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
return {
|
|
uploadChunks,
|
|
downloadChunks,
|
|
}
|
|
}
|
|
|
|
export function verifyPreciseProgress(
|
|
chunks: ChunkProgressLog[],
|
|
expectedChunks: number,
|
|
side: 'upload' | 'download',
|
|
): void {
|
|
expect(chunks.length).toBe(expectedChunks)
|
|
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
const chunk = chunks[i]
|
|
|
|
console.log(
|
|
`${side} chunk ${chunk.chunkNumber}: ${chunk.offset}-${chunk.end}/${chunk.fileSize} ` +
|
|
`progress=${chunk.progressPercentage}% final=${chunk.final}`,
|
|
)
|
|
|
|
// Verify chunks are received in order
|
|
expect(chunk.chunkNumber).toBe(i + 1)
|
|
|
|
// Verify progress is monotonically increasing
|
|
if (i > 0) {
|
|
expect(chunk.progressPercentage).toBeGreaterThanOrEqual(
|
|
chunks[i - 1].progressPercentage,
|
|
)
|
|
}
|
|
|
|
// For the final chunk, ensure we reach exactly 100%
|
|
if (chunk.final) {
|
|
expect(chunk.progressPercentage).toBe(100)
|
|
}
|
|
|
|
// Verify progress percentage is reasonable (0-100%)
|
|
expect(chunk.progressPercentage).toBeGreaterThanOrEqual(0)
|
|
expect(chunk.progressPercentage).toBeLessThanOrEqual(100)
|
|
}
|
|
}
|
|
|