improve code quality and user experience
This commit is contained in:
1
bin/builders/BaseBuilder.ts
vendored
1
bin/builders/BaseBuilder.ts
vendored
@@ -173,6 +173,7 @@ export default abstract class BaseBuilder {
|
|||||||
`cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`,
|
`cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`,
|
||||||
this.getBuildTimeout(),
|
this.getBuildTimeout(),
|
||||||
buildEnv,
|
buildEnv,
|
||||||
|
this.options.debug,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Copy app
|
// Copy app
|
||||||
|
|||||||
20
bin/helpers/merge.ts
vendored
20
bin/helpers/merge.ts
vendored
@@ -7,6 +7,13 @@ import { generateSafeFilename, generateIdentifierSafeName } from '@/utils/name';
|
|||||||
import { PakeAppOptions, PlatformMap } from '@/types';
|
import { PakeAppOptions, PlatformMap } from '@/types';
|
||||||
import { tauriConfigDirectory, npmDirectory } from '@/utils/dir';
|
import { tauriConfigDirectory, npmDirectory } from '@/utils/dir';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to generate safe lowercase app name for file paths
|
||||||
|
*/
|
||||||
|
function getSafeAppName(name: string): string {
|
||||||
|
return generateSafeFilename(name).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
export async function mergeConfig(
|
export async function mergeConfig(
|
||||||
url: string,
|
url: string,
|
||||||
options: PakeAppOptions,
|
options: PakeAppOptions,
|
||||||
@@ -153,7 +160,7 @@ export async function mergeConfig(
|
|||||||
delete tauriConf.bundle.linux.deb.files;
|
delete tauriConf.bundle.linux.deb.files;
|
||||||
|
|
||||||
// Generate correct desktop file configuration
|
// Generate correct desktop file configuration
|
||||||
const appNameSafe = generateSafeFilename(name).toLowerCase();
|
const appNameSafe = getSafeAppName(name);
|
||||||
const identifier = `com.pake.${appNameSafe}`;
|
const identifier = `com.pake.${appNameSafe}`;
|
||||||
const desktopFileName = `${identifier}.desktop`;
|
const desktopFileName = `${identifier}.desktop`;
|
||||||
|
|
||||||
@@ -212,22 +219,23 @@ StartupNotify=true
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set icon.
|
// Set icon.
|
||||||
|
const safeAppName = getSafeAppName(name);
|
||||||
const platformIconMap: PlatformMap = {
|
const platformIconMap: PlatformMap = {
|
||||||
win32: {
|
win32: {
|
||||||
fileExt: '.ico',
|
fileExt: '.ico',
|
||||||
path: `png/${generateSafeFilename(name).toLowerCase()}_256.ico`,
|
path: `png/${safeAppName}_256.ico`,
|
||||||
defaultIcon: 'png/icon_256.ico',
|
defaultIcon: 'png/icon_256.ico',
|
||||||
message: 'Windows icon must be .ico and 256x256px.',
|
message: 'Windows icon must be .ico and 256x256px.',
|
||||||
},
|
},
|
||||||
linux: {
|
linux: {
|
||||||
fileExt: '.png',
|
fileExt: '.png',
|
||||||
path: `png/${generateSafeFilename(name).toLowerCase()}_512.png`,
|
path: `png/${safeAppName}_512.png`,
|
||||||
defaultIcon: 'png/icon_512.png',
|
defaultIcon: 'png/icon_512.png',
|
||||||
message: 'Linux icon must be .png and 512x512px.',
|
message: 'Linux icon must be .png and 512x512px.',
|
||||||
},
|
},
|
||||||
darwin: {
|
darwin: {
|
||||||
fileExt: '.icns',
|
fileExt: '.icns',
|
||||||
path: `icons/${generateSafeFilename(name).toLowerCase()}.icns`,
|
path: `icons/${safeAppName}.icns`,
|
||||||
defaultIcon: 'icons/icon.icns',
|
defaultIcon: 'icons/icon.icns',
|
||||||
message: 'macOS icon must be .icns type.',
|
message: 'macOS icon must be .icns type.',
|
||||||
},
|
},
|
||||||
@@ -278,9 +286,9 @@ StartupNotify=true
|
|||||||
if (iconExt == '.png' || iconExt == '.ico') {
|
if (iconExt == '.png' || iconExt == '.ico') {
|
||||||
const trayIcoPath = path.join(
|
const trayIcoPath = path.join(
|
||||||
npmDirectory,
|
npmDirectory,
|
||||||
`src-tauri/png/${generateSafeFilename(name).toLowerCase()}${iconExt}`,
|
`src-tauri/png/${safeAppName}${iconExt}`,
|
||||||
);
|
);
|
||||||
trayIconPath = `png/${generateSafeFilename(name).toLowerCase()}${iconExt}`;
|
trayIconPath = `png/${safeAppName}${iconExt}`;
|
||||||
await fsExtra.copy(systemTrayIcon, trayIcoPath);
|
await fsExtra.copy(systemTrayIcon, trayIcoPath);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|||||||
21
dist/cli.js
vendored
21
dist/cli.js
vendored
@@ -422,6 +422,12 @@ function generateIdentifierSafeName(name) {
|
|||||||
return cleaned;
|
return cleaned;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to generate safe lowercase app name for file paths
|
||||||
|
*/
|
||||||
|
function getSafeAppName(name) {
|
||||||
|
return generateSafeFilename(name).toLowerCase();
|
||||||
|
}
|
||||||
async function mergeConfig(url, options, tauriConf) {
|
async function mergeConfig(url, options, tauriConf) {
|
||||||
// Ensure .pake directory exists and copy source templates if needed
|
// Ensure .pake directory exists and copy source templates if needed
|
||||||
const srcTauriDir = path.join(npmDirectory, 'src-tauri');
|
const srcTauriDir = path.join(npmDirectory, 'src-tauri');
|
||||||
@@ -512,7 +518,7 @@ async function mergeConfig(url, options, tauriConf) {
|
|||||||
// Remove hardcoded desktop files and regenerate with correct app name
|
// Remove hardcoded desktop files and regenerate with correct app name
|
||||||
delete tauriConf.bundle.linux.deb.files;
|
delete tauriConf.bundle.linux.deb.files;
|
||||||
// Generate correct desktop file configuration
|
// Generate correct desktop file configuration
|
||||||
const appNameSafe = generateSafeFilename(name).toLowerCase();
|
const appNameSafe = getSafeAppName(name);
|
||||||
const identifier = `com.pake.${appNameSafe}`;
|
const identifier = `com.pake.${appNameSafe}`;
|
||||||
const desktopFileName = `${identifier}.desktop`;
|
const desktopFileName = `${identifier}.desktop`;
|
||||||
// Create desktop file content
|
// Create desktop file content
|
||||||
@@ -563,22 +569,23 @@ StartupNotify=true
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Set icon.
|
// Set icon.
|
||||||
|
const safeAppName = getSafeAppName(name);
|
||||||
const platformIconMap = {
|
const platformIconMap = {
|
||||||
win32: {
|
win32: {
|
||||||
fileExt: '.ico',
|
fileExt: '.ico',
|
||||||
path: `png/${generateSafeFilename(name).toLowerCase()}_256.ico`,
|
path: `png/${safeAppName}_256.ico`,
|
||||||
defaultIcon: 'png/icon_256.ico',
|
defaultIcon: 'png/icon_256.ico',
|
||||||
message: 'Windows icon must be .ico and 256x256px.',
|
message: 'Windows icon must be .ico and 256x256px.',
|
||||||
},
|
},
|
||||||
linux: {
|
linux: {
|
||||||
fileExt: '.png',
|
fileExt: '.png',
|
||||||
path: `png/${generateSafeFilename(name).toLowerCase()}_512.png`,
|
path: `png/${safeAppName}_512.png`,
|
||||||
defaultIcon: 'png/icon_512.png',
|
defaultIcon: 'png/icon_512.png',
|
||||||
message: 'Linux icon must be .png and 512x512px.',
|
message: 'Linux icon must be .png and 512x512px.',
|
||||||
},
|
},
|
||||||
darwin: {
|
darwin: {
|
||||||
fileExt: '.icns',
|
fileExt: '.icns',
|
||||||
path: `icons/${generateSafeFilename(name).toLowerCase()}.icns`,
|
path: `icons/${safeAppName}.icns`,
|
||||||
defaultIcon: 'icons/icon.icns',
|
defaultIcon: 'icons/icon.icns',
|
||||||
message: 'macOS icon must be .icns type.',
|
message: 'macOS icon must be .icns type.',
|
||||||
},
|
},
|
||||||
@@ -622,8 +629,8 @@ StartupNotify=true
|
|||||||
// 需要判断图标格式,默认只支持ico和png两种
|
// 需要判断图标格式,默认只支持ico和png两种
|
||||||
let iconExt = path.extname(systemTrayIcon).toLowerCase();
|
let iconExt = path.extname(systemTrayIcon).toLowerCase();
|
||||||
if (iconExt == '.png' || iconExt == '.ico') {
|
if (iconExt == '.png' || iconExt == '.ico') {
|
||||||
const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${generateSafeFilename(name).toLowerCase()}${iconExt}`);
|
const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${safeAppName}${iconExt}`);
|
||||||
trayIconPath = `png/${generateSafeFilename(name).toLowerCase()}${iconExt}`;
|
trayIconPath = `png/${safeAppName}${iconExt}`;
|
||||||
await fsExtra.copy(systemTrayIcon, trayIcoPath);
|
await fsExtra.copy(systemTrayIcon, trayIcoPath);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -799,7 +806,7 @@ class BaseBuilder {
|
|||||||
...this.getBuildEnvironment(),
|
...this.getBuildEnvironment(),
|
||||||
...(process.env.NO_STRIP && { NO_STRIP: process.env.NO_STRIP }),
|
...(process.env.NO_STRIP && { NO_STRIP: process.env.NO_STRIP }),
|
||||||
};
|
};
|
||||||
await shellExec(`cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`, this.getBuildTimeout(), buildEnv);
|
await shellExec(`cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`, this.getBuildTimeout(), buildEnv, this.options.debug);
|
||||||
// Copy app
|
// Copy app
|
||||||
const fileName = this.getFileName();
|
const fileName = this.getFileName();
|
||||||
const fileType = this.getFileType(target);
|
const fileType = this.getFileType(target);
|
||||||
|
|||||||
@@ -38,6 +38,44 @@ document.addEventListener("keydown", (e) => {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Built-in Features
|
||||||
|
|
||||||
|
### Download Error Notifications
|
||||||
|
|
||||||
|
Pake automatically provides user-friendly download error notifications:
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- **Bilingual Support**: Automatically detects browser language (Chinese/English)
|
||||||
|
- **System Notifications**: Uses native OS notifications when permission is granted
|
||||||
|
- **Graceful Fallback**: Falls back to console logging if notifications are unavailable
|
||||||
|
- **Comprehensive Coverage**: Handles all download types (HTTP, Data URI, Blob)
|
||||||
|
|
||||||
|
**User Experience:**
|
||||||
|
|
||||||
|
When a download fails, users will see a notification:
|
||||||
|
|
||||||
|
- English: "Download Error - Download failed: filename.pdf"
|
||||||
|
- Chinese: "下载错误 - 下载失败: filename.pdf"
|
||||||
|
|
||||||
|
**Requesting Notification Permission:**
|
||||||
|
|
||||||
|
To enable notifications, add this to your injected JavaScript:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Request notification permission on app start
|
||||||
|
if (window.Notification && Notification.permission === "default") {
|
||||||
|
Notification.requestPermission();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The download system automatically handles:
|
||||||
|
|
||||||
|
- Regular HTTP(S) downloads
|
||||||
|
- Data URI downloads (base64 encoded files)
|
||||||
|
- Blob URL downloads (dynamically generated files)
|
||||||
|
- Context menu initiated downloads
|
||||||
|
|
||||||
## Container Communication
|
## Container Communication
|
||||||
|
|
||||||
Send messages between web content and Pake container.
|
Send messages between web content and Pake container.
|
||||||
|
|||||||
@@ -40,6 +40,41 @@
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 内置功能
|
||||||
|
|
||||||
|
### 下载错误通知
|
||||||
|
|
||||||
|
Pake 自动提供用户友好的下载错误通知:
|
||||||
|
|
||||||
|
**功能特性:**
|
||||||
|
- **双语支持**:自动检测浏览器语言(中文/英文)
|
||||||
|
- **系统通知**:在授予权限后使用原生操作系统通知
|
||||||
|
- **优雅降级**:如果通知不可用则降级到控制台日志
|
||||||
|
- **全面覆盖**:处理所有下载类型(HTTP、Data URI、Blob)
|
||||||
|
|
||||||
|
**用户体验:**
|
||||||
|
|
||||||
|
当下载失败时,用户将看到通知:
|
||||||
|
- 英文:"Download Error - Download failed: filename.pdf"
|
||||||
|
- 中文:"下载错误 - 下载失败: filename.pdf"
|
||||||
|
|
||||||
|
**请求通知权限:**
|
||||||
|
|
||||||
|
要启用通知,请在注入的 JavaScript 中添加:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在应用启动时请求通知权限
|
||||||
|
if (window.Notification && Notification.permission === "default") {
|
||||||
|
Notification.requestPermission();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
下载系统自动处理:
|
||||||
|
- 常规 HTTP(S) 下载
|
||||||
|
- Data URI 下载(base64 编码文件)
|
||||||
|
- Blob URL 下载(动态生成的文件)
|
||||||
|
- 右键菜单发起的下载
|
||||||
|
|
||||||
## 容器通信
|
## 容器通信
|
||||||
|
|
||||||
在网页内容和 Pake 容器之间发送消息。
|
在网页内容和 Pake 容器之间发送消息。
|
||||||
|
|||||||
34
src-tauri/src/inject/event.js
vendored
34
src-tauri/src/inject/event.js
vendored
@@ -148,6 +148,22 @@ function isChineseLanguage(language = getUserLanguage()) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User notification helper
|
||||||
|
function showDownloadError(filename) {
|
||||||
|
const isChinese = isChineseLanguage();
|
||||||
|
const message = isChinese
|
||||||
|
? `下载失败: ${filename}`
|
||||||
|
: `Download failed: ${filename}`;
|
||||||
|
|
||||||
|
if (window.Notification && Notification.permission === "granted") {
|
||||||
|
new Notification(isChinese ? "下载错误" : "Download Error", {
|
||||||
|
body: message,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Unified file detection - replaces both isDownloadLink and isFileLink
|
// Unified file detection - replaces both isDownloadLink and isFileLink
|
||||||
function isDownloadableFile(url) {
|
function isDownloadableFile(url) {
|
||||||
try {
|
try {
|
||||||
@@ -251,7 +267,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// write the ArrayBuffer to a binary, and you're done
|
// write the ArrayBuffer to a binary, and you're done
|
||||||
const userLanguage = navigator.language || navigator.userLanguage;
|
const userLanguage = getUserLanguage();
|
||||||
invoke("download_file_by_binary", {
|
invoke("download_file_by_binary", {
|
||||||
params: {
|
params: {
|
||||||
filename,
|
filename,
|
||||||
@@ -260,16 +276,18 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
},
|
},
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error("Failed to download data URI file:", filename, error);
|
console.error("Failed to download data URI file:", filename, error);
|
||||||
|
showDownloadError(filename);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to process data URI:", dataURI, error);
|
console.error("Failed to process data URI:", dataURI, error);
|
||||||
|
showDownloadError(filename || "file");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadFromBlobUrl(blobUrl, filename) {
|
function downloadFromBlobUrl(blobUrl, filename) {
|
||||||
convertBlobUrlToBinary(blobUrl)
|
convertBlobUrlToBinary(blobUrl)
|
||||||
.then((binary) => {
|
.then((binary) => {
|
||||||
const userLanguage = navigator.language || navigator.userLanguage;
|
const userLanguage = getUserLanguage();
|
||||||
invoke("download_file_by_binary", {
|
invoke("download_file_by_binary", {
|
||||||
params: {
|
params: {
|
||||||
filename,
|
filename,
|
||||||
@@ -278,10 +296,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
},
|
},
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error("Failed to download blob file:", filename, error);
|
console.error("Failed to download blob file:", filename, error);
|
||||||
|
showDownloadError(filename);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Failed to convert blob to binary:", blobUrl, error);
|
console.error("Failed to convert blob to binary:", blobUrl, error);
|
||||||
|
showDownloadError(filename);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,13 +679,16 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Regular HTTP(S) image
|
// Regular HTTP(S) image
|
||||||
const userLanguage = navigator.language || navigator.userLanguage;
|
const userLanguage = getUserLanguage();
|
||||||
invoke("download_file", {
|
invoke("download_file", {
|
||||||
params: {
|
params: {
|
||||||
url: imageUrl,
|
url: imageUrl,
|
||||||
filename: filename,
|
filename: filename,
|
||||||
language: userLanguage,
|
language: userLanguage,
|
||||||
},
|
},
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Failed to download image:", filename, error);
|
||||||
|
showDownloadError(filename);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -713,7 +736,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
// Simplified menu builder
|
// Simplified menu builder
|
||||||
function buildMenuItems(type, data) {
|
function buildMenuItems(type, data) {
|
||||||
const userLanguage = navigator.language || navigator.userLanguage;
|
const userLanguage = getUserLanguage();
|
||||||
const items = [];
|
const items = [];
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -740,6 +763,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const filename = getFilenameFromUrl(data.url);
|
const filename = getFilenameFromUrl(data.url);
|
||||||
invoke("download_file", {
|
invoke("download_file", {
|
||||||
params: { url: data.url, filename, language: userLanguage },
|
params: { url: data.url, filename, language: userLanguage },
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Failed to download file:", filename, error);
|
||||||
|
showDownloadError(filename);
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import fs from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import ora from "ora";
|
import ora from "ora";
|
||||||
import config, { TIMEOUTS, TEST_URLS } from "./config.js";
|
import config, { TIMEOUTS, TEST_URLS } from "./config.js";
|
||||||
|
import { runHelperTests } from "./unit/helpers.test.js";
|
||||||
|
|
||||||
class PakeTestRunner {
|
class PakeTestRunner {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -42,6 +43,12 @@ class PakeTestRunner {
|
|||||||
console.log("📋 Running Unit Tests...");
|
console.log("📋 Running Unit Tests...");
|
||||||
await this.runUnitTests();
|
await this.runUnitTests();
|
||||||
testCount++;
|
testCount++;
|
||||||
|
|
||||||
|
// Run helper function tests
|
||||||
|
const helperTestsPassed = await runHelperTests();
|
||||||
|
if (!helperTestsPassed) {
|
||||||
|
console.log("⚠️ Some helper tests failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (integration && !quick) {
|
if (integration && !quick) {
|
||||||
|
|||||||
232
tests/unit/helpers.test.js
Normal file
232
tests/unit/helpers.test.js
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit Tests for Helper Functions
|
||||||
|
*
|
||||||
|
* Tests for newly added helper functions to ensure code quality
|
||||||
|
* and prevent regressions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { strict as assert } from 'assert';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to generate safe filename
|
||||||
|
* Mirrors the implementation in bin/utils/name.ts
|
||||||
|
*/
|
||||||
|
function generateSafeFilename(name) {
|
||||||
|
return name
|
||||||
|
.replace(/[<>:"/\\|?*]/g, '_')
|
||||||
|
.replace(/\s+/g, '_')
|
||||||
|
.replace(/\.+$/g, '')
|
||||||
|
.slice(0, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to generate safe lowercase app name for file paths
|
||||||
|
* Mirrors the implementation in bin/helpers/merge.ts
|
||||||
|
*/
|
||||||
|
function getSafeAppName(name) {
|
||||||
|
return generateSafeFilename(name).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test suite for getSafeAppName()
|
||||||
|
*/
|
||||||
|
export function testGetSafeAppName() {
|
||||||
|
const tests = [
|
||||||
|
// Basic cases
|
||||||
|
{ input: 'MyApp', expected: 'myapp', description: 'Simple name' },
|
||||||
|
{ input: 'My App', expected: 'my_app', description: 'Name with space' },
|
||||||
|
{ input: 'my-app', expected: 'my-app', description: 'Name with hyphen' },
|
||||||
|
|
||||||
|
// Chinese characters
|
||||||
|
{ input: '我的应用', expected: '我的应用', description: 'Chinese name' },
|
||||||
|
{ input: '我的 App', expected: '我的_app', description: 'Mixed Chinese and English' },
|
||||||
|
|
||||||
|
// Special characters
|
||||||
|
{ input: 'App@2024', expected: 'app@2024', description: 'Special character @ (preserved)' },
|
||||||
|
{ input: 'My/App', expected: 'my_app', description: 'Forward slash' },
|
||||||
|
{ input: 'My\\App', expected: 'my_app', description: 'Backslash' },
|
||||||
|
{ input: 'App:Name', expected: 'app_name', description: 'Colon' },
|
||||||
|
{ input: 'App*Name', expected: 'app_name', description: 'Asterisk' },
|
||||||
|
{ input: 'App?Name', expected: 'app_name', description: 'Question mark' },
|
||||||
|
{ input: 'App"Name', expected: 'app_name', description: 'Double quote' },
|
||||||
|
{ input: 'App<Name>', expected: 'app_name_', description: 'Angle brackets' },
|
||||||
|
{ input: 'App|Name', expected: 'app_name', description: 'Pipe' },
|
||||||
|
|
||||||
|
// Edge cases
|
||||||
|
{ input: 'APP', expected: 'app', description: 'All uppercase' },
|
||||||
|
{ input: 'a', expected: 'a', description: 'Single character' },
|
||||||
|
{ input: '123', expected: '123', description: 'Numbers only' },
|
||||||
|
{ input: ' App ', expected: '_app_', description: 'Leading/trailing spaces (collapsed)' },
|
||||||
|
{ input: 'App...', expected: 'app', description: 'Trailing dots' },
|
||||||
|
|
||||||
|
// Long names
|
||||||
|
{
|
||||||
|
input: 'A'.repeat(300),
|
||||||
|
expected: 'a'.repeat(255),
|
||||||
|
description: 'Very long name (should truncate to 255)',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
console.log('\n🧪 Testing getSafeAppName()');
|
||||||
|
console.log('─'.repeat(50));
|
||||||
|
|
||||||
|
tests.forEach((test, index) => {
|
||||||
|
try {
|
||||||
|
const result = getSafeAppName(test.input);
|
||||||
|
assert.equal(result, test.expected, `Expected "${test.expected}", got "${result}"`);
|
||||||
|
console.log(` ✓ Test ${index + 1}: ${test.description}`);
|
||||||
|
passed++;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` ✗ Test ${index + 1}: ${test.description}`);
|
||||||
|
console.log(` Input: "${test.input}"`);
|
||||||
|
console.log(` Expected: "${test.expected}"`);
|
||||||
|
console.log(` Error: ${error.message}`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('─'.repeat(50));
|
||||||
|
console.log(`Results: ${passed} passed, ${failed} failed\n`);
|
||||||
|
|
||||||
|
return failed === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test suite for download error notification (browser environment simulation)
|
||||||
|
*/
|
||||||
|
export function testDownloadErrorNotification() {
|
||||||
|
console.log('\n🧪 Testing showDownloadError() Logic');
|
||||||
|
console.log('─'.repeat(50));
|
||||||
|
|
||||||
|
const tests = [
|
||||||
|
{
|
||||||
|
name: 'Chinese language detection',
|
||||||
|
language: 'zh-CN',
|
||||||
|
filename: 'test.pdf',
|
||||||
|
expectedTitle: '下载错误',
|
||||||
|
expectedBody: '下载失败: test.pdf',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'English language detection',
|
||||||
|
language: 'en-US',
|
||||||
|
filename: 'document.docx',
|
||||||
|
expectedTitle: 'Download Error',
|
||||||
|
expectedBody: 'Download failed: document.docx',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Traditional Chinese',
|
||||||
|
language: 'zh-TW',
|
||||||
|
filename: 'file.zip',
|
||||||
|
expectedTitle: '下载错误',
|
||||||
|
expectedBody: '下载失败: file.zip',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Hong Kong Chinese',
|
||||||
|
language: 'zh-HK',
|
||||||
|
filename: 'image.png',
|
||||||
|
expectedTitle: '下载错误',
|
||||||
|
expectedBody: '下载失败: image.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Special characters in filename',
|
||||||
|
language: 'en-US',
|
||||||
|
filename: 'my file (1).pdf',
|
||||||
|
expectedTitle: 'Download Error',
|
||||||
|
expectedBody: 'Download failed: my file (1).pdf',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
tests.forEach((test, index) => {
|
||||||
|
try {
|
||||||
|
// Simulate language detection
|
||||||
|
const isChineseLanguage = (lang) =>
|
||||||
|
lang &&
|
||||||
|
(lang.startsWith('zh') ||
|
||||||
|
lang.includes('CN') ||
|
||||||
|
lang.includes('TW') ||
|
||||||
|
lang.includes('HK'));
|
||||||
|
|
||||||
|
const isChinese = isChineseLanguage(test.language);
|
||||||
|
const title = isChinese ? '下载错误' : 'Download Error';
|
||||||
|
const body = isChinese
|
||||||
|
? `下载失败: ${test.filename}`
|
||||||
|
: `Download failed: ${test.filename}`;
|
||||||
|
|
||||||
|
assert.equal(title, test.expectedTitle, `Title mismatch for ${test.name}`);
|
||||||
|
assert.equal(body, test.expectedBody, `Body mismatch for ${test.name}`);
|
||||||
|
|
||||||
|
console.log(` ✓ Test ${index + 1}: ${test.name}`);
|
||||||
|
console.log(` Language: ${test.language}`);
|
||||||
|
console.log(` Title: "${title}"`);
|
||||||
|
console.log(` Body: "${body}"`);
|
||||||
|
passed++;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` ✗ Test ${index + 1}: ${test.name}`);
|
||||||
|
console.log(` Error: ${error.message}`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('─'.repeat(50));
|
||||||
|
console.log(`Results: ${passed} passed, ${failed} failed\n`);
|
||||||
|
|
||||||
|
return failed === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run all tests
|
||||||
|
*/
|
||||||
|
export async function runHelperTests() {
|
||||||
|
console.log('\n📦 Helper Functions Unit Tests');
|
||||||
|
console.log('='.repeat(50));
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
// Test getSafeAppName
|
||||||
|
results.push({
|
||||||
|
name: 'getSafeAppName()',
|
||||||
|
passed: testGetSafeAppName(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test download error notification
|
||||||
|
results.push({
|
||||||
|
name: 'showDownloadError() Logic',
|
||||||
|
passed: testDownloadErrorNotification(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
const allPassed = results.every((r) => r.passed);
|
||||||
|
const passedCount = results.filter((r) => r.passed).length;
|
||||||
|
const totalCount = results.length;
|
||||||
|
|
||||||
|
console.log('\n📊 Helper Tests Summary');
|
||||||
|
console.log('='.repeat(50));
|
||||||
|
results.forEach((result) => {
|
||||||
|
const icon = result.passed ? '✅' : '❌';
|
||||||
|
console.log(`${icon} ${result.name}`);
|
||||||
|
});
|
||||||
|
console.log('='.repeat(50));
|
||||||
|
console.log(`Total: ${passedCount}/${totalCount} test suites passed\n`);
|
||||||
|
|
||||||
|
return allPassed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run tests if executed directly
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
runHelperTests()
|
||||||
|
.then((success) => {
|
||||||
|
process.exit(success ? 0 : 1);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Test execution failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user