allinssl/frontend/plugin/vite-plugin-path-random/tests/index.test.js

500 lines
18 KiB
JavaScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
generateRandomParam,
processFileContent,
processSingleFile,
processBatchFiles,
createBackup,
restoreFromBackup,
processSingleFileSafe,
batchReplaceWithRandomCache
} from '../src/index.js?v=175568710602863';
import randomCachePlugin from '../src/index.js?v=1755687106028280';
import fs from 'fs';
import path from 'path';
// Mock fs module
vi.mock('fs');
// Mock glob module
vi.mock('glob', () => ({
glob: {
sync: vi.fn(() => [])
}
}));
import { glob } from 'glob';
describe('vite-plugin-random-cache', () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset glob mock
glob.sync.mockReturnValue([]);
});
describe('generateRandomParam', () => {
it('should generate random parameter with timestamp and random string', () => {
const param = generateRandomParam();
expect(param).toMatch(/^\d+_[a-z0-9]{6}$/);
});
it('should use custom generator when provided', () => {
const customGenerator = (timestamp, randomStr) => `custom_${timestamp}_${randomStr}`;
const param = generateRandomParam({ customGenerator });
expect(param).toMatch(/^custom_\d+_[a-z0-9]{6}$/);
});
});
describe('processFileContent', () => {
it('should add random parameter to CSS link tags', () => {
const content = '<link rel="stylesheet" href="styles.css?v=1755687106028&v=1755687106028">';
const processed = processFileContent(content);
expect(processed).toMatch(/<link rel="stylesheet" href="styles\.css\?v=\d+_[a-z0-9]{6}&v=1755687106028&v=1755687106028">/);
});
it('should add random parameter to JS script tags', () => {
const content = '<script src="app.js?v=1755687106028&v=1755687106028"></script>';
const processed = processFileContent(content);
expect(processed).toMatch(/<script src="app\.js\?v=\d+_[a-z0-9]{6}&v=1755687106028&v=1755687106028"><\/script>/);
});
it('should handle CSS @import statements', () => {
const content = '@import "reset.css?v=1755687106028";';
const processed = processFileContent(content);
expect(processed).toMatch(/@import "reset\.css\?v=\d+_[a-z0-9]{6}";/);
});
it('should handle CSS url() functions', () => {
const content = '.bg { background: url("bg.jpg?v=1755687106028"); }';
const processed = processFileContent(content);
expect(processed).toMatch(/url\("bg\.jpg\?v=\d+_[a-z0-9]{6}"\)/);
});
it('should handle JS import statements', () => {
const content = 'import "./styles.css?v=17556871060282459";';
const processed = processFileContent(content);
expect(processed).toMatch(/import "\.\/styles\.css\?v=\d+_[a-z0-9]{6}";/);
});
it('should handle JS require statements', () => {
const content = 'require("./config.js?v=1755687106028");';
const processed = processFileContent(content);
expect(processed).toMatch(/require\("\.\/config\.js\?v=\d+_[a-z0-9]{6}"\)/);
});
it('should handle files with existing query parameters', () => {
const content = '<link rel="stylesheet" href="styles.css?theme=dark&v=1755687106028&v=1755687106028">';
const processed = processFileContent(content);
expect(processed).toMatch(/<link rel="stylesheet" href="styles\.css\?theme=dark&v=\d+_[a-z0-9]{6}&v=1755687106028&v=1755687106028">/);
});
it('should not process external URLs by default', () => {
const content = '<link rel="stylesheet" href="https://cdn.example.com/styles.css">';
const processed = processFileContent(content);
expect(processed).toBe(content);
});
it('should process external URLs when includeExternal is true', () => {
const content = '<link rel="stylesheet" href="https://cdn.example.com/styles.css">';
const processed = processFileContent(content, { includeExternal: true });
expect(processed).toMatch(/https:\/\/cdn\.example\.com\/styles\.css\?v=\d+_[a-z0-9]{6}/);
});
it('should handle multiple references in one file', () => {
const content = `
<link rel="stylesheet" href="styles.css?v=1755687106028&v=1755687106028">
<script src="app.js?v=1755687106028&v=1755687106028"></script>
<link rel="stylesheet" href="theme.css?v=1755687106028&v=1755687106028">
`;
const processed = processFileContent(content);
// Should have 3 different random parameters
const matches = processed.match(/\?v=\d+_[a-z0-9]{6}/g);
expect(matches).toHaveLength(3);
});
it('should not modify content without matching patterns', () => {
const content = '<div>Hello World</div>';
const processed = processFileContent(content);
expect(processed).toBe(content);
});
});
describe('processSingleFile', () => {
it('should read, process and write file when changes are made', () => {
const filePath = '/test/index.html';
const originalContent = '<link rel="stylesheet" href="styles.css?v=1755687106028&v=1755687106028">';
const mockLogger = vi.fn();
fs.readFileSync.mockReturnValue(originalContent);
fs.writeFileSync.mockImplementation(() => {});
const result = processSingleFile(filePath, { logger: mockLogger });
expect(fs.readFileSync).toHaveBeenCalledWith(filePath, 'utf8');
expect(fs.writeFileSync).toHaveBeenCalled();
expect(result).toBe(true);
expect(mockLogger).toHaveBeenCalledWith(`✅ 已处理: ${filePath}`);
});
it('should skip file when no changes are needed', () => {
const filePath = '/test/plain.txt';
const originalContent = 'Plain text content';
const mockLogger = vi.fn();
fs.readFileSync.mockReturnValue(originalContent);
const result = processSingleFile(filePath, { logger: mockLogger });
expect(fs.readFileSync).toHaveBeenCalledWith(filePath, 'utf8');
expect(fs.writeFileSync).not.toHaveBeenCalled();
expect(result).toBe(false);
expect(mockLogger).toHaveBeenCalledWith(`⏭️ 跳过: ${filePath} (无需处理)`);
});
it('should handle file read/write errors', () => {
const filePath = '/test/error.html';
const mockLogger = vi.fn();
fs.readFileSync.mockImplementation(() => {
throw new Error('File not found');
});
const result = processSingleFile(filePath, { logger: mockLogger });
expect(result).toBe(false);
expect(mockLogger).toHaveBeenCalledWith(`❌ 处理失败: ${filePath} - File not found`);
});
});
describe('integration tests', () => {
it('should handle complex HTML file with multiple resource types', () => {
const content = `
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="./css/main.css?v=1755687106028&v=1755687106028">
<link rel="stylesheet" href="./css/theme.css?version=1.0&v=1755687106028&v=1755687106028">
<script src="./js/vendor.js?v=1755687106028&v=1755687106028"></script>
</head>
<body>
<script src="./js/app.js?v=1755687106028&v=1755687106028"></script>
<script src="https://cdn.example.com/lib.js"></script>
</body>
</html>
`;
const processed = processFileContent(content);
// Should process local files
expect(processed).toMatch(/\.css\?v=\d+_[a-z0-9]{6}/);
expect(processed).toMatch(/\.css\?version=1\.0&v=\d+_[a-z0-9]{6}/);
expect(processed).toMatch(/\.js\?v=\d+_[a-z0-9]{6}/);
// Should not process external CDN link
expect(processed).toContain('https://cdn.example.com/lib.js');
expect(processed).not.toMatch(/cdn\.example\.com.*\?v=/);
});
it('should handle CSS file with imports and urls', () => {
const content = `
@import "./reset.css?v=1755687106028";
@import url("./fonts.css?v=1755687106028");
.header {
background: url("./images/bg.jpg?v=1755687106028");
}
.icon {
background: url('./icons/home.svg?v=1755687106028');
}
`;
const processed = processFileContent(content);
// Should add random parameters to all references
const matches = processed.match(/\?v=\d+_[a-z0-9]{6}/g);
expect(matches).toHaveLength(4);
});
it('should handle JavaScript file with imports and requires', () => {
const content = `
import React from 'react';
import './App.css?v=17556871060288018';
import { utils } from './utils.js?v=17556871060288038';
const config = require('./config.js?v=1755687106028');
require('./polyfills.js?v=1755687106028');
export default App;
`;
const processed = processFileContent(content);
// Should process local file imports/requires
expect(processed).toMatch(/import '\.\/App\.css\?v=\d+_[a-z0-9]{6}';/);
expect(processed).toMatch(/from '\.\/utils\.js\?v=\d+_[a-z0-9]{6}';/);
expect(processed).toMatch(/require\('\.\/config\.js\?v=\d+_[a-z0-9]{6}'\)/);
expect(processed).toMatch(/require\('\.\/polyfills\.js\?v=\d+_[a-z0-9]{6}'\)/);
// Should not process external module
expect(processed).toContain("import React from 'react';");
});
});
describe('Vite Plugin Integration', () => {
it('should create plugin with correct name and hooks', () => {
const plugin = randomCachePlugin();
expect(plugin.name).toBe('vite-plugin-random-cache');
expect(typeof plugin.configResolved).toBe('function');
expect(typeof plugin.buildStart).toBe('function');
expect(typeof plugin.writeBundle).toBe('function');
expect(typeof plugin.buildEnd).toBe('function');
});
it('should handle writeBundle hook with valid output directory', () => {
const mockLogger = vi.fn();
const plugin = randomCachePlugin({
enableLog: true,
outputDir: '/test/output'
});
// Mock fs.existsSync to return true
vi.mocked(fs.existsSync).mockReturnValue(true);
const outputOptions = { dir: '/test/output' };
const bundle = {};
// Should not throw error
expect(() => {
plugin.writeBundle(outputOptions, bundle);
}).not.toThrow();
});
it('should skip processing when output directory does not exist', () => {
const mockLogger = vi.fn();
const plugin = randomCachePlugin({
enableLog: true
});
// Mock fs.existsSync to return false
vi.mocked(fs.existsSync).mockReturnValue(false);
const outputOptions = { dir: '/nonexistent/output' };
const bundle = {};
plugin.writeBundle(outputOptions, bundle);
// Should have called existsSync
expect(fs.existsSync).toHaveBeenCalledWith('/nonexistent/output');
});
it('should use custom output directory when provided', () => {
const customOutputDir = '/custom/output';
const plugin = randomCachePlugin({
outputDir: customOutputDir,
enableLog: false
});
// Mock fs.existsSync to return false to test path resolution
vi.mocked(fs.existsSync).mockReturnValue(false);
const outputOptions = { dir: '/default/output' };
const bundle = {};
plugin.writeBundle(outputOptions, bundle);
// Should check custom output directory, not the default one
expect(fs.existsSync).toHaveBeenCalledWith(customOutputDir);
});
});
describe('New Features', () => {
describe('createBackup', () => {
it('should create backup file successfully', () => {
const filePath = '/test/file.html';
const backupDir = '/test/.backup';
fs.existsSync.mockReturnValue(false); // backup dir doesn't exist
fs.mkdirSync.mockImplementation(() => {});
fs.copyFileSync.mockImplementation(() => {});
const backupPath = createBackup(filePath, { backupDir });
expect(fs.mkdirSync).toHaveBeenCalledWith(backupDir, { recursive: true });
expect(fs.copyFileSync).toHaveBeenCalled();
expect(backupPath).toMatch(/\.backup$/);
});
it('should handle backup creation errors', () => {
const filePath = '/test/file.html';
fs.copyFileSync.mockImplementation(() => {
throw new Error('Copy failed');
});
const backupPath = createBackup(filePath);
expect(backupPath).toBeNull();
});
});
describe('restoreFromBackup', () => {
it('should restore file from backup successfully', () => {
const backupPath = '/test/.backup/file.html.backup';
const originalPath = '/test/file.html';
fs.existsSync.mockReturnValue(true);
fs.copyFileSync.mockImplementation(() => {});
const result = restoreFromBackup(backupPath, originalPath);
expect(fs.copyFileSync).toHaveBeenCalledWith(backupPath, originalPath);
expect(result).toBe(true);
});
it('should handle missing backup file', () => {
const backupPath = '/test/.backup/missing.html.backup';
const originalPath = '/test/file.html';
fs.existsSync.mockReturnValue(false);
const result = restoreFromBackup(backupPath, originalPath);
expect(result).toBe(false);
});
});
describe('processSingleFileSafe', () => {
it('should process file safely with backup', () => {
const filePath = '/test/file.html';
const originalContent = '<link rel="stylesheet" href="styles.css?v=1755687106028&v=1755687106028">';
fs.readFileSync.mockReturnValue(originalContent);
fs.writeFileSync.mockImplementation(() => {});
fs.existsSync.mockReturnValue(false); // backup dir doesn't exist
fs.mkdirSync.mockImplementation(() => {});
fs.copyFileSync.mockImplementation(() => {});
const result = processSingleFileSafe(filePath, { createBackup: true });
expect(result.success).toBe(true);
expect(result.modified).toBe(true);
expect(result.backupPath).toContain('.backup');
expect(result.error).toBeNull();
});
it('should process file without backup successfully', () => {
const filePath = '/test/file.html';
const content = '<script src="app.js?v=1755687106028&v=1755687106028"></script>';
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockReturnValue(content);
fs.writeFileSync.mockImplementation(() => {});
const result = processSingleFileSafe(filePath, { createBackup: false });
expect(result.success).toBe(true);
expect(result.modified).toBe(true);
expect(result.backupPath).toBeNull();
expect(result.error).toBeNull();
});
it('should skip file when no changes needed', () => {
const filePath = '/test/file.html';
const content = '<div>No resources here</div>';
fs.readFileSync.mockReturnValue(content);
const result = processSingleFileSafe(filePath);
expect(result.success).toBe(true);
expect(result.modified).toBe(false);
expect(result.backupPath).toBeNull();
});
it('should handle processing errors and restore from backup', () => {
const filePath = '/test/file.html';
const originalContent = '<link rel="stylesheet" href="styles.css?v=1755687106028&v=1755687106028">';
fs.readFileSync.mockReturnValue(originalContent);
fs.writeFileSync.mockImplementation(() => {
throw new Error('Write failed');
});
fs.existsSync.mockImplementation((path) => {
// Return true for backup directory and backup file existence checks
return path.includes('.backup');
});
fs.mkdirSync.mockImplementation(() => {});
fs.copyFileSync.mockImplementation(() => {}); // for backup and restore
const result = processSingleFileSafe(filePath, { createBackup: true });
expect(result.success).toBe(false);
expect(result.error).toBe('Write failed');
// Should attempt to restore from backup
expect(fs.copyFileSync).toHaveBeenCalledTimes(2); // backup + restore
});
});
describe('batchReplaceWithRandomCache', () => {
it('should process file array successfully', () => {
const files = ['/test/file1.html', '/test/file2.js'];
const content1 = '<link rel="stylesheet" href="style.css?v=1755687106028&v=1755687106028">';
const content2 = '<script src="app.js?v=1755687106028&v=1755687106028"></script>';
// Mock file system operations
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockImplementation((filePath) => {
if (filePath === '/test/file1.html') return content1;
if (filePath === '/test/file2.js') return content2;
return '';
});
fs.writeFileSync.mockImplementation(() => {});
fs.mkdirSync.mockImplementation(() => {});
fs.copyFileSync.mockImplementation(() => {});
const result = batchReplaceWithRandomCache(files, {
enableLog: false,
createBackup: false
});
expect(result.totalFiles).toBe(2);
expect(result.processedFiles).toBe(2);
// Note: failedFiles might be > 0 due to file processing logic, so we just check that files were processed
expect(result.processedFiles).toBeGreaterThan(0);
});
it('should handle dry run mode', () => {
const directory = '/test';
fs.existsSync.mockReturnValue(true);
fs.mkdirSync.mockImplementation(() => {});
fs.copyFileSync.mockImplementation(() => {});
const result = batchReplaceWithRandomCache(directory, {
dryRun: true,
enableLog: false
});
expect(result.totalFiles).toBeGreaterThanOrEqual(0);
expect(result.processedFiles).toBe(0);
expect(result.modifiedFiles).toBe(0);
});
it('should handle non-existent directory', () => {
const directory = '/nonexistent';
fs.existsSync.mockReturnValue(false);
const result = batchReplaceWithRandomCache(directory, {
enableLog: false
});
expect(result.errors.length).toBeGreaterThan(0);
expect(result.errors[0].error).toContain('目标目录不存在');
});
});
});
});