Skip to content

Conversation

@Robdel12
Copy link
Contributor

Why

Currently, our SDKs require users to pass a Buffer for screenshots, which creates unnecessary friction in the developer experience. Many testing frameworks (Playwright, Cypress, Puppeteer) save screenshots directly to disk, forcing developers to:

  1. Save the screenshot to a file
  2. Read the file back into a Buffer
  3. Pass that Buffer to vizzlyScreenshot()

This PR eliminates step 2 by allowing users to pass file paths directly to the SDK. This is especially valuable when:

  • Screenshots are already saved to disk by the testing framework
  • Users want to process existing screenshot files
  • Integrating with tools that generate PNG files

What Changed

SDK Updates

  • Modified vizzlyScreenshot() to accept Buffer | string (client SDK)
  • Updated SDK screenshot() and compare() methods to accept Buffer | string
  • Added automatic file detection using typeof check
  • Implemented file reading with proper error handling using VizzlyError

Test Coverage

  • Added 7 comprehensive tests to tests/sdk/sdk-core.spec.js
  • Created tests/unit/client-file-path.spec.js with 5 client-specific tests
  • Tests cover: absolute paths, relative paths, error handling, backward compatibility

Documentation

  • Updated README.md with both Buffer and file path examples
  • Enhanced docs/test-integration.md with framework-specific examples
  • Updated docs/api-reference.md with complete API documentation
  • Treated both approaches as first-class citizens (no "traditional" vs "new" labeling)

Backward Compatibility

✅ Fully backward compatible - all existing Buffer usage continues to work unchanged. All 584 tests pass.

Examples

Before (still supported)

const screenshot = await page.screenshot();
await vizzlyScreenshot('homepage', screenshot);

After (now also supported)

await page.screenshot({ path: './screenshots/homepage.png' });
await vizzlyScreenshot('homepage', './screenshots/homepage.png');

Resolves VIZ-58

@claude
Copy link

claude bot commented Oct 12, 2025

PR Review: Add File Path Support to SDK and Client

Thanks for this PR @Robdel12! This is a valuable UX improvement that eliminates unnecessary file I/O for developers. I've reviewed the changes and have some feedback below.


Strengths

  1. Excellent backward compatibility - The typeof check elegantly supports both Buffer and string without breaking existing usage
  2. Comprehensive test coverage - 12 new tests across both SDK and client covering various scenarios
  3. Good error handling - Proper use of VizzlyError with error codes and context
  4. Clear documentation - All docs updated with examples for both approaches

🐛 Bugs & Issues

1. Missing fs imports in client SDK (Critical)

Location: src/client/index.js:268-286

The client code uses existsSync and readFileSync but doesn't import them:

// Missing at top of file:
import { existsSync, readFileSync } from 'fs';

Impact: Runtime error when using file paths with the client SDK.

Fix: Add the import statement.


2. Path resolution inconsistency

Location: src/client/index.js and src/sdk/index.js

The code accepts relative paths but doesn't resolve them to absolute paths before checking existence. This could lead to inconsistent behavior depending on the current working directory.

// Current:
if (!existsSync(filePath)) {
  throw new Error(`Screenshot file not found: ${filePath}`);
}

// Suggested:
import { resolve } from 'path';
const resolvedPath = resolve(filePath);
if (!existsSync(resolvedPath)) {
  throw new Error(`Screenshot file not found: ${filePath} (resolved: ${resolvedPath})`);
}
buffer = readFileSync(resolvedPath);

Impact: Error messages may be confusing, and file lookups might fail in edge cases.


🔒 Security Considerations

3. Path traversal validation (Medium)

While this isn't a web-facing API, consider adding basic path validation to prevent accidental issues:

import { resolve, normalize } from 'path';

// Validate path doesn't contain suspicious patterns
const normalizedPath = normalize(filePath);
if (normalizedPath.includes('\0') || normalizedPath.includes('..')) {
  throw new VizzlyError(
    'Invalid file path: path traversal detected',
    'INVALID_PATH',
    { filePath }
  );
}

Rationale: Defense in depth, especially if user input could flow into the file path parameter in some scenarios.


4. File type validation

The docs mention "Works with any PNG image file" but there's no validation that the file is actually a PNG:

// Optional: Add PNG magic number validation
const buffer = readFileSync(resolvedPath);
const isPNG = buffer[0] === 0x89 && buffer[1] === 0x50 && 
              buffer[2] === 0x4E && buffer[3] === 0x47;
if (!isPNG) {
  throw new VizzlyError(
    'File is not a valid PNG image',
    'INVALID_IMAGE_FORMAT',
    { filePath }
  );
}

Impact: Could catch user errors early and provide clearer error messages.


Performance Considerations

5. Synchronous file I/O

Location: Both src/client/index.js and src/sdk/index.js

Using readFileSync blocks the event loop. While this is probably fine for most use cases (screenshots in tests), consider:

  • Document this behavior in JSDoc
  • Consider offering async alternative in the future
  • For large screenshot files (>10MB), this could cause noticeable delays

Current impact: Low (test environments typically aren't I/O constrained)


🧪 Test Coverage

Good:

  • Tests cover absolute paths, relative paths, missing files, and backward compatibility
  • Both SDK and client tested separately
  • Error cases properly tested

Suggestions:

  1. Add test for invalid file content
it('should handle non-PNG files gracefully', async () => {
  const textFilePath = join(testDir, 'not-an-image.txt');
  writeFileSync(textFilePath, 'This is not a PNG');
  
  // Currently this would pass - should it fail or just upload invalid data?
  await vizzlyScreenshot('invalid-file', textFilePath);
});
  1. Add test for symbolic links
it('should handle symbolic links to images', async () => {
  const symlinkPath = join(testDir, 'symlink.png');
  symlinkSync(testImagePath, symlinkPath);
  
  await expect(vizzlyScreenshot('symlink-test', symlinkPath)).resolves.not.toThrow();
});
  1. Test file permissions errors
it('should handle permission errors gracefully', async () => {
  const restrictedPath = join(testDir, 'restricted.png');
  writeFileSync(restrictedPath, Buffer.from('data'));
  chmodSync(restrictedPath, 0o000); // Remove all permissions
  
  await expect(
    vizzlyScreenshot('permission-test', restrictedPath)
  ).rejects.toThrow(/Failed to read screenshot file/);
});

📝 Code Quality & Best Practices

9. JSDoc completeness

The JSDoc for vizzlyScreenshot is excellent, but consider adding @throws annotations:

/**
 * ...
 * @throws {Error} When file path is provided but file doesn't exist
 * @throws {Error} When file cannot be read due to permissions or I/O errors
 * @throws {VizzlyError} When SDK operations fail
 */

10. DRY: Duplicated file reading logic

The file reading logic is duplicated across:

  • src/client/index.js (lines 271-283)
  • src/sdk/index.js (lines 244-266 and 386-408)

Suggestion: Extract to a shared utility:

// src/utils/file-helpers.js
import { existsSync, readFileSync } from 'fs';
import { resolve } from 'path';
import { VizzlyError } from '../errors/vizzly-error.js';

/**
 * Read image buffer from file path or return buffer as-is
 * @param {Buffer|string} imageBufferOrPath 
 * @param {string} contextName - For error messages (e.g., 'screenshot', 'compare')
 * @returns {Buffer}
 */
export function resolveImageBuffer(imageBufferOrPath, contextName) {
  if (Buffer.isBuffer(imageBufferOrPath)) {
    return imageBufferOrPath;
  }
  
  if (typeof imageBufferOrPath !== 'string') {
    throw new VizzlyError(
      `Invalid image input: expected Buffer or file path string`,
      'INVALID_INPUT',
      { contextName, type: typeof imageBufferOrPath }
    );
  }
  
  const filePath = resolve(imageBufferOrPath);
  
  if (!existsSync(filePath)) {
    throw new VizzlyError(
      `Screenshot file not found: ${imageBufferOrPath}`,
      'FILE_NOT_FOUND',
      { contextName, filePath }
    );
  }
  
  try {
    return readFileSync(filePath);
  } catch (error) {
    throw new VizzlyError(
      `Failed to read screenshot file: ${imageBufferOrPath} - ${error.message}`,
      'FILE_READ_ERROR',
      { contextName, filePath, originalError: error.message }
    );
  }
}

Then use it:

// In SDK and client
const buffer = resolveImageBuffer(imageBuffer, 'screenshot');

Benefits:

  • Single source of truth for file handling logic
  • Easier to add features (PNG validation, symlink handling, etc.)
  • More maintainable
  • Bonus: Adds type validation for unexpected input types

🎯 Minor Improvements

  1. Error message consistency
  • Client uses Error while SDK uses VizzlyError
  • Client error: "Screenshot file not found: ${filePath}"
  • SDK error: "Screenshot file not found: ${filePath}" (same, but different error types)

Suggestion: Use VizzlyError consistently or document why client uses Error.


  1. Test cleanup could be more robust
    Location: tests/sdk/sdk-core.spec.js:393-398 and tests/unit/client-file-path.spec.js:51-64
afterEach(() => {
  try {
    rmSync(testDir, { recursive: true, force: true });
  } catch (error) {
    // Ignore cleanup errors - but maybe log them in debug mode?
  }
});

Consider: Log errors in CI or add a global test teardown to catch persistent file locks.


📋 Summary

Must Fix Before Merge:

Strongly Recommended:

Nice to Have:


🎉 Overall Assessment

This is a solid PR with a clear use case and good implementation. The main blocker is the missing import statement. Once that's fixed and ideally the shared utility is extracted, this will be ready to merge.

The feature provides real value by reducing boilerplate in test code, and the implementation is clean and well-tested.

Recommendation: Request changes for #1 (critical), suggest #10 and #2 as high-priority improvements before merge.

Allow SDKs to accept file paths in addition to buffers for improved DX.
This is particularly useful when screenshots are already saved to disk by
testing frameworks, eliminating the need to read files into buffers manually.

- Modified vizzlyScreenshot() and SDK screenshot()/compare() to accept Buffer | string
- Added file path detection and reading with proper error handling
- Maintained full backward compatibility with existing Buffer usage
- Added comprehensive test coverage (7 SDK tests, 5 client tests)
- Updated documentation with examples for both approaches

Resolves VIZ-58
- Extract file reading logic into shared `resolveImageBuffer()` utility
- Add path resolution to convert relative paths to absolute paths
- Improve error handling with consistent VizzlyError usage
- Add @throws JSDoc annotations for better API documentation
- Remove duplicated code from client and SDK implementations

This refactoring addresses PR feedback by:
- Providing clear error messages with both original and resolved paths
- Ensuring consistent file lookup behavior across the codebase
- Making future enhancements (like PNG validation) easier to maintain
- Standardizing error types across client and SDK
@Robdel12
Copy link
Contributor Author

@claude can you reevaluate this PR?

@claude
Copy link

claude bot commented Oct 12, 2025

Claude finished @Robdel12's task —— View job


Re-evaluating PR #52

Progress

  • Read current implementation files
  • Check if critical issues were addressed
  • Verify recommended improvements were implemented
  • Review test coverage
  • Provide updated feedback

Review Summary

Great work addressing the feedback! 🎉 I've reviewed the updated PR and I'm pleased to see that all critical issues and most high-priority recommendations have been successfully addressed.

✅ Issues Addressed

1. Critical: Missing fs imports ✅ FIXED

Status: The critical issue has been partially resolved, but there's still an issue:

Location: src/client/index.js:13

The client file now imports existsSync and readFileSync:

import { existsSync, readFileSync } from 'fs';

However, these imports are no longer used in the client file since you've correctly extracted the file reading logic to the shared utility! The client now uses resolveImageBuffer from file-helpers.js (line 12).

Action needed: Remove the unused imports on line 13:

// Remove this line - no longer needed:
import { existsSync, readFileSync } from 'fs';

2. Shared Utility Function ✅ EXCELLENT

Status: Implemented perfectly!

You've created src/utils/file-helpers.js with the resolveImageBuffer() function that:

  • ✅ Handles both Buffer and string inputs
  • ✅ Validates input types with proper error messages
  • ✅ Uses resolve() to convert relative paths to absolute paths
  • ✅ Checks file existence with existsSync()
  • ✅ Uses VizzlyError consistently with error codes
  • ✅ Provides detailed error context (originalPath, filePath, contextName)
  • ✅ Proper try/catch for file reading errors

This is exactly what I recommended in item #10 from my previous review! The implementation is clean, maintainable, and follows best practices.


3. Path Resolution ✅ FIXED

Status: Resolved in the shared utility!

The resolveImageBuffer() function now uses resolve() from the path module (line 43):

const filePath = resolve(imageBufferOrPath);

This ensures consistent behavior regardless of the current working directory. The error messages also include both the original path and the resolved path for debugging.


4. Error Consistency ✅ FIXED

Status: Now consistent across both SDK and client!

Both the SDK and client now use the shared utility, which throws VizzlyError consistently with proper error codes:

  • INVALID_INPUT - For invalid input types
  • FILE_NOT_FOUND - When file doesn't exist
  • FILE_READ_ERROR - When file can't be read

5. Test Coverage ✅ COMPREHENSIVE

Status: Excellent test coverage added!

You've added comprehensive tests:

  • Client tests (tests/unit/client-file-path.spec.js): 5 tests covering absolute paths, relative paths, missing files, Buffer backward compatibility, and options passing
  • SDK tests (tests/sdk/sdk-core.spec.js): 7 tests covering screenshot and compare methods with file paths

The tests verify:

  • ✅ File path input handling
  • ✅ Relative and absolute paths
  • ✅ Error handling for missing files
  • ✅ Backward compatibility with Buffers
  • ✅ Options passing through with file paths
  • ✅ Both screenshot() and compare() methods

📋 Remaining Items from Previous Review

✅ Addressed

⚠️ Not Addressed (But Low Priority)

These are all "nice to have" improvements that can be addressed in future PRs:

Note: None of these remaining items are blockers for merging. They're quality-of-life improvements that can be addressed incrementally.


🐛 Minor Issue to Fix

Unused imports in client:

// src/client/index.js:13 - Remove this line
import { existsSync, readFileSync } from 'fs';

These are no longer needed since you're using the shared utility. This will be caught by linters and should be cleaned up.


🎯 Final Recommendation

Status: ✅ Ready to merge (after removing unused imports)

This PR successfully achieves its goal of allowing file path inputs while maintaining excellent backward compatibility. The implementation is clean, well-tested, and follows the DRY principle with the shared utility function.

What improved from the original review:

  1. ✅ Critical bug fixed (fs imports added, though now unnecessary)
  2. ✅ Shared utility extracted perfectly
  3. ✅ Path resolution implemented correctly
  4. ✅ Error handling made consistent
  5. ✅ Comprehensive test coverage added

Before merging:

  • Remove unused fs imports from src/client/index.js:13
  • Consider running linter to catch any other issues

After merging:

Great work on this PR! The file path support will significantly improve the developer experience for Vizzly users. 🚀


Job Run: View run
Branch: robert/viz-58

@Robdel12 Robdel12 merged commit 30fba20 into main Oct 12, 2025
14 checks passed
@Robdel12 Robdel12 deleted the robert/viz-58 branch October 12, 2025 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants