mirror of
https://github.com/vrcx-team/VRCX.git
synced 2026-04-06 00:32:02 +02:00
feat: add jest testing for utility functions
This commit is contained in:
@@ -34,6 +34,7 @@ export default defineConfig([
|
||||
{
|
||||
files: [
|
||||
'**/webpack.*.js',
|
||||
'**/jest.config.js',
|
||||
'src-electron/*.js',
|
||||
'src/localization/*.js'
|
||||
],
|
||||
@@ -44,6 +45,18 @@ export default defineConfig([
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'**/__tests__/**/*.{js,mjs,cjs,vue}',
|
||||
'**/*.spec.{js,mjs,cjs,vue}',
|
||||
'**/*.test.{js,mjs,cjs,vue}'
|
||||
],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.jest
|
||||
}
|
||||
}
|
||||
},
|
||||
pluginVue.configs['flat/vue2-essential'],
|
||||
{
|
||||
rules: {
|
||||
|
||||
19
jest.config.js
Normal file
19
jest.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
moduleFileExtensions: ['js', 'vue'],
|
||||
transform: {
|
||||
'^.+\\.js$': 'esbuild-jest'
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1'
|
||||
},
|
||||
testMatch: ['<rootDir>/src/**/*.{test,spec}.js'],
|
||||
testPathIgnorePatterns: [],
|
||||
watchPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/build/'],
|
||||
coverageReporters: ['text', 'text-summary'],
|
||||
collectCoverageFrom: [
|
||||
'src/shared/utils/**/*.js',
|
||||
'!src/shared/utils/**/*.test.js',
|
||||
'!src/shared/utils/**/__tests__/**'
|
||||
]
|
||||
};
|
||||
7817
package-lock.json
generated
7817
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,9 +5,12 @@
|
||||
"main": "src-electron/main.js",
|
||||
"scripts": {
|
||||
"dev": "cross-env PLATFORM=windows webpack serve --config webpack.config.js --mode development",
|
||||
"dev:test": "concurrently \"npm run dev\" \"jest --watchAll\"",
|
||||
"watch": "cross-env PLATFORM=windows webpack --config webpack.config.js --mode development --watch",
|
||||
"watch-linux": "cross-env PLATFORM=linux webpack --config webpack.config.js --mode development --watch",
|
||||
"localization": "node ./src/localization/localizationHelperCLI.js",
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --coverage",
|
||||
"prod": "cross-env PLATFORM=windows webpack --config webpack.config.js --mode production",
|
||||
"prod-linux": "cross-env PLATFORM=linux webpack --config webpack.config.js --mode production",
|
||||
"build-electron": "node ./src-electron/download-dotnet-runtime.js && node ./src-electron/patch-package-version.js && electron-builder --publish never",
|
||||
@@ -36,8 +39,10 @@
|
||||
"@fontsource/noto-sans-tc": "^5.2.6",
|
||||
"@infolektuell/noto-color-emoji": "^0.2.0",
|
||||
"@prettier/plugin-pug": "^3.4.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.0.13",
|
||||
"animate.css": "^4.1.1",
|
||||
"concurrently": "^9.2.0",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^7.1.2",
|
||||
@@ -46,12 +51,14 @@
|
||||
"electron": "^37.2.1",
|
||||
"electron-builder": "^26.0.12",
|
||||
"element-ui": "^2.15.14",
|
||||
"esbuild-jest": "^0.5.0",
|
||||
"esbuild-loader": "^4.3.0",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-vue": "^9.33.0",
|
||||
"globals": "^16.3.0",
|
||||
"html-webpack-plugin": "^5.6.3",
|
||||
"jest": "^30.0.4",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"noty": "^3.2.0-beta-deprecated",
|
||||
"pinia": "^2.3.1",
|
||||
|
||||
404
src/shared/utils/__tests__/compare.test.js
Normal file
404
src/shared/utils/__tests__/compare.test.js
Normal file
@@ -0,0 +1,404 @@
|
||||
import {
|
||||
compareByName,
|
||||
compareByCreatedAt,
|
||||
compareByCreatedAtAscending,
|
||||
compareByUpdatedAt,
|
||||
compareByDisplayName,
|
||||
compareByMemberCount,
|
||||
compareByPrivate,
|
||||
compareByStatus,
|
||||
compareByLastActive,
|
||||
compareByLastSeen,
|
||||
compareByLocationAt,
|
||||
compareByLocation
|
||||
} from '../compare';
|
||||
|
||||
describe('Compare Functions', () => {
|
||||
describe('compareByName', () => {
|
||||
test('compares objects by name property', () => {
|
||||
const a = { name: 'Alice' };
|
||||
const b = { name: 'Bob' };
|
||||
expect(compareByName(a, b)).toBeLessThan(0);
|
||||
expect(compareByName(b, a)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('returns 0 for equal names', () => {
|
||||
const a = { name: 'Alice' };
|
||||
const b = { name: 'Alice' };
|
||||
expect(compareByName(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
test('handles non-string name properties', () => {
|
||||
const a = { name: null };
|
||||
const b = { name: 'Bob' };
|
||||
expect(compareByName(a, b)).toBe(0);
|
||||
|
||||
const c = { name: 123 };
|
||||
const d = { name: 'Alice' };
|
||||
expect(compareByName(c, d)).toBe(0);
|
||||
});
|
||||
|
||||
test('handles missing name properties', () => {
|
||||
const a = {};
|
||||
const b = { name: 'Bob' };
|
||||
expect(compareByName(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
test('uses locale-aware comparison', () => {
|
||||
const a = { name: 'ä' };
|
||||
const b = { name: 'z' };
|
||||
expect(typeof compareByName(a, b)).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareByCreatedAt', () => {
|
||||
test('compares objects by created_at in descending order', () => {
|
||||
const a = { created_at: '2023-01-01' };
|
||||
const b = { created_at: '2023-01-02' };
|
||||
expect(compareByCreatedAt(a, b)).toBeGreaterThan(0);
|
||||
expect(compareByCreatedAt(b, a)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
test('returns 0 for equal created_at', () => {
|
||||
const a = { created_at: '2023-01-01' };
|
||||
const b = { created_at: '2023-01-01' };
|
||||
expect(compareByCreatedAt(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
test('handles non-string created_at properties', () => {
|
||||
const a = { created_at: null };
|
||||
const b = { created_at: '2023-01-01' };
|
||||
expect(compareByCreatedAt(a, b)).toBe(0);
|
||||
|
||||
const c = { created_at: 123 };
|
||||
const d = { created_at: '2023-01-01' };
|
||||
expect(compareByCreatedAt(c, d)).toBe(0);
|
||||
});
|
||||
|
||||
test('handles case-insensitive comparison', () => {
|
||||
const a = { created_at: '2023-01-01t12:00:00z' };
|
||||
const b = { created_at: '2023-01-01T12:00:00Z' };
|
||||
expect(compareByCreatedAt(a, b)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareByCreatedAtAscending', () => {
|
||||
test('compares objects by created_at in ascending order', () => {
|
||||
const a = { created_at: '2023-01-01' };
|
||||
const b = { created_at: '2023-01-02' };
|
||||
expect(compareByCreatedAtAscending(a, b)).toBeLessThan(0);
|
||||
expect(compareByCreatedAtAscending(b, a)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('returns 0 for equal created_at', () => {
|
||||
const a = { created_at: '2023-01-01' };
|
||||
const b = { created_at: '2023-01-01' };
|
||||
expect(compareByCreatedAtAscending(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
test('handles undefined created_at', () => {
|
||||
const a = { created_at: undefined };
|
||||
const b = { created_at: '2023-01-01' };
|
||||
// undefined comparison with string: both undefined < string and undefined > string are false
|
||||
// So it falls through to return 0
|
||||
expect(compareByCreatedAtAscending(a, b)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareByUpdatedAt', () => {
|
||||
test('compares objects by updated_at in descending order', () => {
|
||||
const a = { updated_at: '2023-01-01' };
|
||||
const b = { updated_at: '2023-01-02' };
|
||||
expect(compareByUpdatedAt(a, b)).toBeGreaterThan(0);
|
||||
expect(compareByUpdatedAt(b, a)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
test('returns 0 for equal updated_at', () => {
|
||||
const a = { updated_at: '2023-01-01' };
|
||||
const b = { updated_at: '2023-01-01' };
|
||||
expect(compareByUpdatedAt(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
test('handles non-string updated_at properties', () => {
|
||||
const a = { updated_at: null };
|
||||
const b = { updated_at: '2023-01-01' };
|
||||
expect(compareByUpdatedAt(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
test('handles case-insensitive comparison', () => {
|
||||
const a = { updated_at: '2023-01-01t12:00:00z' };
|
||||
const b = { updated_at: '2023-01-01T12:00:00Z' };
|
||||
expect(compareByUpdatedAt(a, b)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareByDisplayName', () => {
|
||||
test('compares objects by displayName property', () => {
|
||||
const a = { displayName: 'Alice Display' };
|
||||
const b = { displayName: 'Bob Display' };
|
||||
expect(compareByDisplayName(a, b)).toBeLessThan(0);
|
||||
expect(compareByDisplayName(b, a)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('returns 0 for equal displayNames', () => {
|
||||
const a = { displayName: 'Alice Display' };
|
||||
const b = { displayName: 'Alice Display' };
|
||||
expect(compareByDisplayName(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
test('handles non-string displayName properties', () => {
|
||||
const a = { displayName: null };
|
||||
const b = { displayName: 'Bob Display' };
|
||||
expect(compareByDisplayName(a, b)).toBe(0);
|
||||
|
||||
const c = { displayName: 123 };
|
||||
const d = { displayName: 'Alice Display' };
|
||||
expect(compareByDisplayName(c, d)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareByMemberCount', () => {
|
||||
test('compares objects by memberCount property', () => {
|
||||
const a = { memberCount: 5 };
|
||||
const b = { memberCount: 10 };
|
||||
expect(compareByMemberCount(a, b)).toBe(-5);
|
||||
expect(compareByMemberCount(b, a)).toBe(5);
|
||||
});
|
||||
|
||||
test('returns 0 for equal memberCounts', () => {
|
||||
const a = { memberCount: 5 };
|
||||
const b = { memberCount: 5 };
|
||||
expect(compareByMemberCount(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
test('handles non-number memberCount properties', () => {
|
||||
const a = { memberCount: 'invalid' };
|
||||
const b = { memberCount: 10 };
|
||||
expect(compareByMemberCount(a, b)).toBe(0);
|
||||
|
||||
const c = { memberCount: null };
|
||||
const d = { memberCount: 5 };
|
||||
expect(compareByMemberCount(c, d)).toBe(0);
|
||||
});
|
||||
|
||||
test('handles negative member counts', () => {
|
||||
const a = { memberCount: -5 };
|
||||
const b = { memberCount: 10 };
|
||||
expect(compareByMemberCount(a, b)).toBe(-15);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareByPrivate', () => {
|
||||
test('prioritizes non-private locations', () => {
|
||||
const a = { ref: { location: 'public' } };
|
||||
const b = { ref: { location: 'private' } };
|
||||
expect(compareByPrivate(a, b)).toBe(-1);
|
||||
expect(compareByPrivate(b, a)).toBe(1);
|
||||
});
|
||||
|
||||
test('returns 0 when both are private', () => {
|
||||
const a = { ref: { location: 'private' } };
|
||||
const b = { ref: { location: 'private' } };
|
||||
expect(compareByPrivate(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
test('returns 0 when both are non-private', () => {
|
||||
const a = { ref: { location: 'public' } };
|
||||
const b = { ref: { location: 'friends' } };
|
||||
expect(compareByPrivate(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
test('handles undefined ref properties', () => {
|
||||
const a = { ref: undefined };
|
||||
const b = { ref: { location: 'private' } };
|
||||
expect(compareByPrivate(a, b)).toBe(0);
|
||||
|
||||
const c = {};
|
||||
const d = { ref: { location: 'public' } };
|
||||
expect(compareByPrivate(c, d)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareByStatus', () => {
|
||||
test('handles offline users', () => {
|
||||
const a = { ref: { state: 'offline', status: 'active' } };
|
||||
const b = { ref: { state: 'online', status: 'busy' } };
|
||||
expect(compareByStatus(a, b)).toBe(1);
|
||||
});
|
||||
|
||||
test('returns 0 for same status', () => {
|
||||
const a = { ref: { status: 'active' } };
|
||||
const b = { ref: { status: 'active' } };
|
||||
expect(compareByStatus(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
test('handles undefined ref properties', () => {
|
||||
const a = { ref: undefined };
|
||||
const b = { ref: { status: 'active' } };
|
||||
expect(compareByStatus(a, b)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareByLastActive', () => {
|
||||
test('compares online users by $online_for', () => {
|
||||
const a = {
|
||||
state: 'online',
|
||||
ref: { $online_for: 100 }
|
||||
};
|
||||
const b = {
|
||||
state: 'online',
|
||||
ref: { $online_for: 200 }
|
||||
};
|
||||
expect(compareByLastActive(a, b)).toBe(1);
|
||||
});
|
||||
|
||||
test('falls back to last_activity for non-online users', () => {
|
||||
const a = {
|
||||
state: 'offline',
|
||||
ref: { last_activity: '2023-01-01' }
|
||||
};
|
||||
const b = {
|
||||
state: 'offline',
|
||||
ref: { last_activity: '2023-01-02' }
|
||||
};
|
||||
expect(compareByLastActive(a, b)).toBe(1);
|
||||
});
|
||||
|
||||
test('handles undefined ref properties', () => {
|
||||
const a = { state: 'online', ref: undefined };
|
||||
const b = { state: 'online', ref: { $online_for: 100 } };
|
||||
expect(compareByLastActive(a, b)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareByLastSeen', () => {
|
||||
test('compares by $lastSeen field', () => {
|
||||
const a = { ref: { $lastSeen: '2023-01-01' } };
|
||||
const b = { ref: { $lastSeen: '2023-01-02' } };
|
||||
expect(compareByLastSeen(a, b)).toBe(1);
|
||||
});
|
||||
|
||||
test('handles empty string as longest active', () => {
|
||||
const a = { ref: { $lastSeen: '' } };
|
||||
const b = { ref: { $lastSeen: '2023-01-01' } };
|
||||
// '' < '2023-01-01' is true, so first condition matches and returns 1
|
||||
expect(compareByLastSeen(a, b)).toBe(1);
|
||||
});
|
||||
|
||||
test('handles undefined ref properties', () => {
|
||||
const a = { ref: undefined };
|
||||
const b = { ref: { $lastSeen: '2023-01-01' } };
|
||||
expect(compareByLastSeen(a, b)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareByLocationAt', () => {
|
||||
test('handles traveling status', () => {
|
||||
const a = { location: 'traveling', $location_at: 100 };
|
||||
const b = { location: 'public', $location_at: 200 };
|
||||
expect(compareByLocationAt(a, b)).toBe(1);
|
||||
expect(compareByLocationAt(b, a)).toBe(-1);
|
||||
});
|
||||
|
||||
test('returns 0 when both are traveling', () => {
|
||||
const a = { location: 'traveling', $location_at: 100 };
|
||||
const b = { location: 'traveling', $location_at: 200 };
|
||||
expect(compareByLocationAt(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
test('compares by $location_at for non-traveling locations', () => {
|
||||
const a = { location: 'public', $location_at: 100 };
|
||||
const b = { location: 'friends', $location_at: 200 };
|
||||
expect(compareByLocationAt(a, b)).toBe(-1);
|
||||
expect(compareByLocationAt(b, a)).toBe(1);
|
||||
});
|
||||
|
||||
test('returns 0 for equal $location_at', () => {
|
||||
const a = { location: 'public', $location_at: 100 };
|
||||
const b = { location: 'friends', $location_at: 100 };
|
||||
expect(compareByLocationAt(a, b)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareByLocation', () => {
|
||||
test('compares online users by location', () => {
|
||||
const a = {
|
||||
state: 'online',
|
||||
ref: { location: 'public' }
|
||||
};
|
||||
const b = {
|
||||
state: 'online',
|
||||
ref: { location: 'friends' }
|
||||
};
|
||||
expect(compareByLocation(a, b)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('returns 0 for non-online users', () => {
|
||||
const a = {
|
||||
state: 'offline',
|
||||
ref: { location: 'public' }
|
||||
};
|
||||
const b = {
|
||||
state: 'offline',
|
||||
ref: { location: 'friends' }
|
||||
};
|
||||
expect(compareByLocation(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
test('returns 0 when one user is not online', () => {
|
||||
const a = {
|
||||
state: 'online',
|
||||
ref: { location: 'public' }
|
||||
};
|
||||
const b = {
|
||||
state: 'offline',
|
||||
ref: { location: 'friends' }
|
||||
};
|
||||
expect(compareByLocation(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
test('handles undefined ref properties', () => {
|
||||
const a = { state: 'online', ref: undefined };
|
||||
const b = { state: 'online', ref: { location: 'public' } };
|
||||
expect(compareByLocation(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
test('uses locale-aware comparison', () => {
|
||||
const a = {
|
||||
state: 'online',
|
||||
ref: { location: 'ä' }
|
||||
};
|
||||
const b = {
|
||||
state: 'online',
|
||||
ref: { location: 'z' }
|
||||
};
|
||||
expect(typeof compareByLocation(a, b)).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases and boundary conditions', () => {
|
||||
test('handles null objects', () => {
|
||||
// compareByName doesn't handle null objects - it will throw
|
||||
expect(() => compareByName(null, { name: 'test' })).toThrow();
|
||||
expect(() => compareByName({ name: 'test' }, null)).toThrow();
|
||||
});
|
||||
|
||||
test('handles empty objects', () => {
|
||||
expect(compareByName({}, {})).toBe(0);
|
||||
expect(compareByDisplayName({}, {})).toBe(0);
|
||||
expect(compareByMemberCount({}, {})).toBe(0);
|
||||
});
|
||||
|
||||
test('handles mixed valid and invalid data', () => {
|
||||
const valid = { memberCount: 5 };
|
||||
const invalid = { memberCount: 'not a number' };
|
||||
expect(compareByMemberCount(valid, invalid)).toBe(0);
|
||||
});
|
||||
|
||||
test('handles zero values', () => {
|
||||
const a = { memberCount: 0 };
|
||||
const b = { memberCount: 5 };
|
||||
expect(compareByMemberCount(a, b)).toBe(-5);
|
||||
});
|
||||
});
|
||||
});
|
||||
28
src/shared/utils/__tests__/friend.test.js
Normal file
28
src/shared/utils/__tests__/friend.test.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { sortStatus, isFriendOnline } from '../friend';
|
||||
|
||||
describe('Friend Utils', () => {
|
||||
describe('sortStatus', () => {
|
||||
test('handles same status', () => {
|
||||
expect(sortStatus('active', 'active')).toBe(0);
|
||||
expect(sortStatus('join me', 'join me')).toBe(0);
|
||||
});
|
||||
|
||||
test('handles unknown status', () => {
|
||||
expect(sortStatus('unknown', 'active')).toBe(0);
|
||||
// @ts-ignore
|
||||
expect(sortStatus(null, 'active')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFriendOnline', () => {
|
||||
test('detects online friends', () => {
|
||||
const friend = { state: 'online', ref: { location: 'world' } };
|
||||
expect(isFriendOnline(friend)).toBe(true);
|
||||
});
|
||||
|
||||
test('handles missing data', () => {
|
||||
expect(isFriendOnline({})).toBe(false);
|
||||
expect(isFriendOnline({ state: 'online' })).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
314
src/shared/utils/__tests__/gallery.test.js
Normal file
314
src/shared/utils/__tests__/gallery.test.js
Normal file
@@ -0,0 +1,314 @@
|
||||
import {
|
||||
getPrintFileName,
|
||||
getPrintLocalDate,
|
||||
getEmojiFileName
|
||||
} from '../gallery';
|
||||
|
||||
describe('Gallery Utils', () => {
|
||||
describe('getPrintFileName', () => {
|
||||
test('generates filename with createdAt', () => {
|
||||
const print = {
|
||||
authorName: 'TestUser',
|
||||
createdAt: '2023-01-15T10:30:45.123Z',
|
||||
id: 'print_12345'
|
||||
};
|
||||
const result = getPrintFileName(print);
|
||||
expect(result).toContain('TestUser');
|
||||
expect(result).toContain('print_12345');
|
||||
expect(result).toContain('.png');
|
||||
// Check date formatting - should replace : with - and T with _
|
||||
expect(result).toMatch(
|
||||
/\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.\d{3}/
|
||||
);
|
||||
});
|
||||
|
||||
test('generates filename with timestamp fallback', () => {
|
||||
const print = {
|
||||
authorName: 'TestUser2',
|
||||
timestamp: 1673776245123, // 2023-01-15T10:30:45.123Z
|
||||
id: 'print_67890'
|
||||
};
|
||||
const result = getPrintFileName(print);
|
||||
expect(result).toContain('TestUser2');
|
||||
expect(result).toContain('print_67890');
|
||||
expect(result).toContain('.png');
|
||||
});
|
||||
|
||||
test('generates filename with current date fallback', () => {
|
||||
const print = {
|
||||
authorName: 'TestUser3',
|
||||
id: 'print_fallback'
|
||||
};
|
||||
const result = getPrintFileName(print);
|
||||
expect(result).toContain('TestUser3');
|
||||
expect(result).toContain('print_fallback');
|
||||
expect(result).toContain('.png');
|
||||
// Should still have valid date format
|
||||
expect(result).toMatch(
|
||||
/\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.\d{3}/
|
||||
);
|
||||
});
|
||||
|
||||
test('handles missing authorName', () => {
|
||||
const print = {
|
||||
createdAt: '2023-01-15T10:30:45.123Z',
|
||||
id: 'print_no_author'
|
||||
};
|
||||
const result = getPrintFileName(print);
|
||||
expect(result).toContain('undefined_'); // authorName will be undefined
|
||||
expect(result).toContain('print_no_author');
|
||||
});
|
||||
|
||||
test('handles special characters in authorName', () => {
|
||||
const print = {
|
||||
authorName: 'Test User / With : Chars',
|
||||
createdAt: '2023-01-15T10:30:45.123Z',
|
||||
id: 'print_special'
|
||||
};
|
||||
const result = getPrintFileName(print);
|
||||
expect(result).toContain('Test User / With : Chars'); // Should preserve original chars
|
||||
expect(result).toContain('print_special');
|
||||
});
|
||||
|
||||
test('handles missing data gracefully', () => {
|
||||
const print = { id: 'test' };
|
||||
const result = getPrintFileName(print);
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result).toContain('test');
|
||||
expect(result).toContain('.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPrintLocalDate', () => {
|
||||
test('converts createdAt to local date', () => {
|
||||
const print = { createdAt: '2023-01-15T10:30:45.123Z' };
|
||||
const result = getPrintLocalDate(print);
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
// Should have timezone offset applied
|
||||
const originalDate = new Date('2023-01-15T10:30:45.123Z');
|
||||
const expectedLocalTime =
|
||||
originalDate.getTime() -
|
||||
originalDate.getTimezoneOffset() * 60000;
|
||||
expect(result.getTime()).toBe(expectedLocalTime);
|
||||
});
|
||||
|
||||
test('uses timestamp without timezone conversion', () => {
|
||||
const timestamp = 1673776245123; // 2023-01-15T10:30:45.123Z
|
||||
const print = { timestamp };
|
||||
const result = getPrintLocalDate(print);
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
expect(result.getTime()).toBe(timestamp);
|
||||
});
|
||||
|
||||
test('prefers createdAt over timestamp', () => {
|
||||
const print = {
|
||||
createdAt: '2023-01-15T10:30:45.123Z',
|
||||
timestamp: 1673000000000 // Different timestamp
|
||||
};
|
||||
const result = getPrintLocalDate(print);
|
||||
// Should use createdAt, not timestamp
|
||||
const originalDate = new Date('2023-01-15T10:30:45.123Z');
|
||||
const expectedLocalTime =
|
||||
originalDate.getTime() -
|
||||
originalDate.getTimezoneOffset() * 60000;
|
||||
expect(result.getTime()).toBe(expectedLocalTime);
|
||||
});
|
||||
|
||||
test('defaults to current date with timezone adjustment', () => {
|
||||
const print = {};
|
||||
const result = getPrintLocalDate(print);
|
||||
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
// Should be approximately current time with timezone offset
|
||||
const currentDate = new Date();
|
||||
const expectedLocalTime =
|
||||
currentDate.getTime() - currentDate.getTimezoneOffset() * 60000;
|
||||
// Allow some tolerance for execution time
|
||||
expect(Math.abs(result.getTime() - expectedLocalTime)).toBeLessThan(
|
||||
1000
|
||||
);
|
||||
});
|
||||
|
||||
test('handles invalid createdAt gracefully', () => {
|
||||
const print = { createdAt: 'invalid-date' };
|
||||
const result = getPrintLocalDate(print);
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
// Should fall back to current date when createdAt is invalid
|
||||
});
|
||||
|
||||
test('handles null/undefined inputs', () => {
|
||||
expect(getPrintLocalDate({})).toBeInstanceOf(Date);
|
||||
expect(getPrintLocalDate({ createdAt: null })).toBeInstanceOf(Date);
|
||||
expect(getPrintLocalDate({ timestamp: null })).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEmojiFileName', () => {
|
||||
test('creates animated filename with all properties', () => {
|
||||
const emoji = {
|
||||
name: 'happy',
|
||||
animationStyle: 'bounce',
|
||||
frames: 10,
|
||||
framesOverTime: 30,
|
||||
loopStyle: 'loop'
|
||||
};
|
||||
const result = getEmojiFileName(emoji);
|
||||
expect(result).toBe(
|
||||
'happy_bounceanimationStyle_10frames_30fps_looploopStyle.png'
|
||||
);
|
||||
});
|
||||
|
||||
test('creates animated filename with default loopStyle', () => {
|
||||
const emoji = {
|
||||
name: 'wave',
|
||||
animationStyle: 'wiggle',
|
||||
frames: 5,
|
||||
framesOverTime: 15
|
||||
};
|
||||
const result = getEmojiFileName(emoji);
|
||||
expect(result).toBe(
|
||||
'wave_wiggleanimationStyle_5frames_15fps_linearloopStyle.png'
|
||||
);
|
||||
});
|
||||
|
||||
test('creates static filename without frames', () => {
|
||||
const emoji = {
|
||||
name: 'smile',
|
||||
animationStyle: 'static'
|
||||
};
|
||||
const result = getEmojiFileName(emoji);
|
||||
expect(result).toBe('smile_staticanimationStyle.png');
|
||||
});
|
||||
|
||||
test('treats zero frames as static', () => {
|
||||
const emoji = {
|
||||
name: 'neutral',
|
||||
animationStyle: 'none',
|
||||
frames: 0,
|
||||
framesOverTime: 0
|
||||
};
|
||||
const result = getEmojiFileName(emoji);
|
||||
expect(result).toBe('neutral_noneanimationStyle.png');
|
||||
});
|
||||
|
||||
test('handles missing name gracefully', () => {
|
||||
const emoji = {
|
||||
animationStyle: 'bounce',
|
||||
frames: 5,
|
||||
framesOverTime: 10
|
||||
};
|
||||
const result = getEmojiFileName(emoji);
|
||||
expect(result).toContain('undefined_bounceanimationStyle');
|
||||
});
|
||||
|
||||
test('handles missing animationStyle', () => {
|
||||
const emoji = {
|
||||
name: 'test',
|
||||
frames: 3,
|
||||
framesOverTime: 20
|
||||
};
|
||||
const result = getEmojiFileName(emoji);
|
||||
expect(result).toContain('test_undefinedanimationStyle');
|
||||
});
|
||||
|
||||
test('handles special characters in name', () => {
|
||||
const emoji = {
|
||||
name: 'emoji-with_special.chars',
|
||||
animationStyle: 'bounce',
|
||||
frames: 8,
|
||||
framesOverTime: 24,
|
||||
loopStyle: 'pingpong'
|
||||
};
|
||||
const result = getEmojiFileName(emoji);
|
||||
expect(result).toBe(
|
||||
'emoji-with_special.chars_bounceanimationStyle_8frames_24fps_pingpongloopStyle.png'
|
||||
);
|
||||
});
|
||||
|
||||
test('handles edge case values', () => {
|
||||
const emoji = {
|
||||
name: '',
|
||||
animationStyle: '',
|
||||
frames: -1,
|
||||
framesOverTime: 0,
|
||||
loopStyle: ''
|
||||
};
|
||||
const result = getEmojiFileName(emoji);
|
||||
// Should still generate a valid filename structure
|
||||
expect(result).toContain('animationStyle');
|
||||
expect(result).toContain('.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration and edge cases', () => {
|
||||
test('getPrintFileName produces valid file names', () => {
|
||||
const testCases = [
|
||||
{
|
||||
print: {
|
||||
authorName: 'ValidUser',
|
||||
createdAt: '2023-12-31T23:59:59.999Z',
|
||||
id: 'print_test123'
|
||||
},
|
||||
expectValid: true
|
||||
},
|
||||
{
|
||||
print: {
|
||||
authorName: '',
|
||||
createdAt: '2023-01-01T00:00:00.000Z',
|
||||
id: ''
|
||||
},
|
||||
expectValid: true
|
||||
},
|
||||
{
|
||||
print: {},
|
||||
expectValid: true
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ print, expectValid }) => {
|
||||
const result = getPrintFileName(print);
|
||||
if (expectValid) {
|
||||
expect(result).toMatch(/.*\.png$/);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('date functions handle timezone consistency', () => {
|
||||
const testDate = '2023-06-15T12:00:00.000Z';
|
||||
const print = { createdAt: testDate };
|
||||
|
||||
const localDate = getPrintLocalDate(print);
|
||||
const fileName = getPrintFileName({
|
||||
...print,
|
||||
authorName: 'test',
|
||||
id: 'test'
|
||||
});
|
||||
|
||||
// Both functions should produce consistent results
|
||||
expect(localDate).toBeInstanceOf(Date);
|
||||
expect(fileName).toMatch(
|
||||
/test_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.\d{3}_test\.png/
|
||||
);
|
||||
});
|
||||
|
||||
test('emoji filename handles boundary conditions', () => {
|
||||
const extremeCases = [
|
||||
{ name: 'a'.repeat(100), animationStyle: 'test' },
|
||||
{ name: 'テスト', animationStyle: 'unicode' },
|
||||
{
|
||||
name: 'test',
|
||||
animationStyle: 'test',
|
||||
frames: 999,
|
||||
framesOverTime: 999
|
||||
}
|
||||
];
|
||||
|
||||
extremeCases.forEach((emoji) => {
|
||||
const result = getEmojiFileName(emoji);
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result.endsWith('.png')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
411
src/shared/utils/__tests__/location.test.js
Normal file
411
src/shared/utils/__tests__/location.test.js
Normal file
@@ -0,0 +1,411 @@
|
||||
import { parseLocation, displayLocation } from '../location';
|
||||
|
||||
describe('Location Utils', () => {
|
||||
describe('parseLocation', () => {
|
||||
test('parses simple world ID', () => {
|
||||
const worldId = 'wrld_12345678-1234-1234-1234-123456789012';
|
||||
const result = parseLocation(worldId);
|
||||
expect(result.worldId).toBe(worldId);
|
||||
expect(result.instanceId).toBe('');
|
||||
expect(result.isOffline).toBe(false);
|
||||
expect(result.isPrivate).toBe(false);
|
||||
expect(result.isTraveling).toBe(false);
|
||||
expect(result.isRealInstance).toBe(true);
|
||||
expect(result.accessType).toBe(''); // No instance means no access type
|
||||
});
|
||||
|
||||
test('parses world with basic instance', () => {
|
||||
const location = 'wrld_12345:67890';
|
||||
const result = parseLocation(location);
|
||||
expect(result.worldId).toBe('wrld_12345');
|
||||
expect(result.instanceId).toBe('67890');
|
||||
expect(result.instanceName).toBe('67890');
|
||||
expect(result.accessType).toBe('public');
|
||||
expect(result.isRealInstance).toBe(true);
|
||||
});
|
||||
|
||||
test('parses instance with region', () => {
|
||||
const location = 'wrld_12345:67890~region(us)';
|
||||
const result = parseLocation(location);
|
||||
expect(result.worldId).toBe('wrld_12345');
|
||||
expect(result.instanceId).toBe('67890~region(us)');
|
||||
expect(result.instanceName).toBe('67890');
|
||||
expect(result.region).toBe('us');
|
||||
expect(result.accessType).toBe('public');
|
||||
});
|
||||
|
||||
test('parses private instance', () => {
|
||||
const location = 'wrld_12345:instance~private(usr_12345)';
|
||||
const result = parseLocation(location);
|
||||
expect(result.worldId).toBe('wrld_12345');
|
||||
expect(result.accessType).toBe('invite');
|
||||
expect(result.privateId).toBe('usr_12345');
|
||||
expect(result.userId).toBe('usr_12345');
|
||||
expect(result.canRequestInvite).toBe(false);
|
||||
});
|
||||
|
||||
test('parses invite+ instance', () => {
|
||||
const location =
|
||||
'wrld_12345:instance~private(usr_12345)~canRequestInvite';
|
||||
const result = parseLocation(location);
|
||||
expect(result.worldId).toBe('wrld_12345');
|
||||
expect(result.accessType).toBe('invite+');
|
||||
expect(result.privateId).toBe('usr_12345');
|
||||
expect(result.userId).toBe('usr_12345');
|
||||
expect(result.canRequestInvite).toBe(true);
|
||||
});
|
||||
|
||||
test('parses friends only instance', () => {
|
||||
const location = 'wrld_12345:instance~friends(usr_67890)';
|
||||
const result = parseLocation(location);
|
||||
expect(result.worldId).toBe('wrld_12345');
|
||||
expect(result.accessType).toBe('friends');
|
||||
expect(result.friendsId).toBe('usr_67890');
|
||||
expect(result.userId).toBe('usr_67890');
|
||||
});
|
||||
|
||||
test('parses friends+ instance', () => {
|
||||
const location = 'wrld_12345:instance~hidden(usr_99999)';
|
||||
const result = parseLocation(location);
|
||||
expect(result.worldId).toBe('wrld_12345');
|
||||
expect(result.accessType).toBe('friends+');
|
||||
expect(result.hiddenId).toBe('usr_99999');
|
||||
expect(result.userId).toBe('usr_99999');
|
||||
});
|
||||
|
||||
test('parses group instance', () => {
|
||||
const location = 'wrld_12345:instance~group(grp_12345)';
|
||||
const result = parseLocation(location);
|
||||
expect(result.worldId).toBe('wrld_12345');
|
||||
expect(result.accessType).toBe('group');
|
||||
expect(result.groupId).toBe('grp_12345');
|
||||
expect(result.accessTypeName).toBe('group');
|
||||
});
|
||||
|
||||
test('parses group public instance', () => {
|
||||
const location =
|
||||
'wrld_12345:instance~group(grp_12345)~groupAccessType(public)';
|
||||
const result = parseLocation(location);
|
||||
expect(result.worldId).toBe('wrld_12345');
|
||||
expect(result.accessType).toBe('group');
|
||||
expect(result.groupId).toBe('grp_12345');
|
||||
expect(result.groupAccessType).toBe('public');
|
||||
expect(result.accessTypeName).toBe('groupPublic');
|
||||
});
|
||||
|
||||
test('parses instance with strict flag', () => {
|
||||
const location = 'wrld_12345:instance~strict';
|
||||
const result = parseLocation(location);
|
||||
expect(result.strict).toBe(true);
|
||||
});
|
||||
|
||||
test('parses instance with ageGate flag', () => {
|
||||
const location = 'wrld_12345:instance~ageGate';
|
||||
const result = parseLocation(location);
|
||||
expect(result.ageGate).toBe(true);
|
||||
});
|
||||
|
||||
test('parses complex instance with multiple parameters', () => {
|
||||
const location =
|
||||
'wrld_12345:67890~region(eu)~private(usr_abc)~canRequestInvite~strict~ageGate';
|
||||
const result = parseLocation(location);
|
||||
expect(result.worldId).toBe('wrld_12345');
|
||||
expect(result.instanceName).toBe('67890');
|
||||
expect(result.region).toBe('eu');
|
||||
expect(result.accessType).toBe('invite+');
|
||||
expect(result.privateId).toBe('usr_abc');
|
||||
expect(result.canRequestInvite).toBe(true);
|
||||
expect(result.strict).toBe(true);
|
||||
expect(result.ageGate).toBe(true);
|
||||
});
|
||||
|
||||
test('parses instance with shortName in URL', () => {
|
||||
const location = 'wrld_12345:67890&shortName=TestInstance';
|
||||
const result = parseLocation(location);
|
||||
expect(result.worldId).toBe('wrld_12345');
|
||||
expect(result.instanceId).toBe('67890');
|
||||
expect(result.shortName).toBe('TestInstance');
|
||||
});
|
||||
|
||||
test('detects special states with variations', () => {
|
||||
expect(parseLocation('offline').isOffline).toBe(true);
|
||||
expect(parseLocation('offline:offline').isOffline).toBe(true);
|
||||
expect(parseLocation('private').isPrivate).toBe(true);
|
||||
expect(parseLocation('private:private').isPrivate).toBe(true);
|
||||
expect(parseLocation('traveling').isTraveling).toBe(true);
|
||||
expect(parseLocation('traveling:traveling').isTraveling).toBe(true);
|
||||
});
|
||||
|
||||
test('handles empty and null inputs', () => {
|
||||
expect(parseLocation(null).tag).toBe('');
|
||||
expect(parseLocation(undefined).tag).toBe('');
|
||||
expect(parseLocation('').tag).toBe('');
|
||||
|
||||
[null, undefined, ''].forEach((input) => {
|
||||
const result = parseLocation(input);
|
||||
expect(result.isOffline).toBe(false);
|
||||
expect(result.isPrivate).toBe(false);
|
||||
expect(result.isTraveling).toBe(false);
|
||||
expect(result.isRealInstance).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('handles malformed instance parameters', () => {
|
||||
const location =
|
||||
'wrld_12345:instance~private()~region~invalid(test';
|
||||
const result = parseLocation(location);
|
||||
expect(result.worldId).toBe('wrld_12345');
|
||||
expect(result.privateId).toBe(''); // Empty parentheses
|
||||
expect(result.region).toBe(''); // No parentheses
|
||||
});
|
||||
|
||||
test('preserves original tag', () => {
|
||||
const originalTag = 'wrld_12345:instance~region(us)';
|
||||
const result = parseLocation(originalTag);
|
||||
expect(result.tag).toBe(originalTag);
|
||||
});
|
||||
});
|
||||
|
||||
describe('displayLocation', () => {
|
||||
test('shows world name for simple world ID', () => {
|
||||
const result = displayLocation('wrld_12345', 'Test World');
|
||||
expect(result).toBe('Test World');
|
||||
});
|
||||
|
||||
test('handles offline state', () => {
|
||||
expect(displayLocation('offline', 'Some World')).toBe('Offline');
|
||||
expect(displayLocation('offline:offline', 'Some World')).toBe(
|
||||
'Offline'
|
||||
);
|
||||
});
|
||||
|
||||
test('handles private state', () => {
|
||||
expect(displayLocation('private', 'Some World')).toBe('Private');
|
||||
expect(displayLocation('private:private', 'Some World')).toBe(
|
||||
'Private'
|
||||
);
|
||||
});
|
||||
|
||||
test('handles traveling state', () => {
|
||||
expect(displayLocation('traveling', 'Some World')).toBe(
|
||||
'Traveling'
|
||||
);
|
||||
expect(displayLocation('traveling:traveling', 'Some World')).toBe(
|
||||
'Traveling'
|
||||
);
|
||||
});
|
||||
|
||||
test('shows world with access type for instance', () => {
|
||||
const result = displayLocation(
|
||||
'wrld_12345:instance~private(usr_123)',
|
||||
'Test World'
|
||||
);
|
||||
expect(result).toBe('Test World invite');
|
||||
});
|
||||
|
||||
test('includes group name when provided', () => {
|
||||
const result = displayLocation(
|
||||
'wrld_12345:instance~group(grp_123)',
|
||||
'Test World',
|
||||
'My Group'
|
||||
);
|
||||
expect(result).toBe('Test World group(My Group)');
|
||||
});
|
||||
|
||||
test('shows different access types correctly', () => {
|
||||
const worldName = 'Test World';
|
||||
|
||||
expect(displayLocation('wrld_12345:instance', worldName)).toBe(
|
||||
'Test World public'
|
||||
);
|
||||
expect(
|
||||
displayLocation(
|
||||
'wrld_12345:instance~private(usr_123)',
|
||||
worldName
|
||||
)
|
||||
).toBe('Test World invite');
|
||||
expect(
|
||||
displayLocation(
|
||||
'wrld_12345:instance~private(usr_123)~canRequestInvite',
|
||||
worldName
|
||||
)
|
||||
).toBe('Test World invite+');
|
||||
expect(
|
||||
displayLocation(
|
||||
'wrld_12345:instance~friends(usr_123)',
|
||||
worldName
|
||||
)
|
||||
).toBe('Test World friends');
|
||||
expect(
|
||||
displayLocation(
|
||||
'wrld_12345:instance~hidden(usr_123)',
|
||||
worldName
|
||||
)
|
||||
).toBe('Test World friends+');
|
||||
expect(
|
||||
displayLocation('wrld_12345:instance~group(grp_123)', worldName)
|
||||
).toBe('Test World group');
|
||||
});
|
||||
|
||||
test('shows group access types correctly', () => {
|
||||
const worldName = 'Test World';
|
||||
const groupName = 'Test Group';
|
||||
|
||||
expect(
|
||||
displayLocation(
|
||||
'wrld_12345:instance~group(grp_123)~groupAccessType(public)',
|
||||
worldName,
|
||||
groupName
|
||||
)
|
||||
).toBe('Test World groupPublic(Test Group)');
|
||||
expect(
|
||||
displayLocation(
|
||||
'wrld_12345:instance~group(grp_123)~groupAccessType(plus)',
|
||||
worldName,
|
||||
groupName
|
||||
)
|
||||
).toBe('Test World groupPlus(Test Group)');
|
||||
});
|
||||
|
||||
test('prioritizes group name over access type when both available', () => {
|
||||
const result = displayLocation(
|
||||
'wrld_12345:instance~private(usr_123)',
|
||||
'Test World',
|
||||
'Override Group'
|
||||
);
|
||||
expect(result).toBe('Test World invite(Override Group)');
|
||||
});
|
||||
|
||||
test('handles empty or missing world name', () => {
|
||||
expect(displayLocation('wrld_12345:instance', '')).toBe(' public');
|
||||
expect(displayLocation('wrld_12345:instance', undefined)).toBe(
|
||||
'undefined public'
|
||||
);
|
||||
});
|
||||
|
||||
test('handles empty or missing group name', () => {
|
||||
const result = displayLocation(
|
||||
'wrld_12345:instance~group(grp_123)',
|
||||
'Test World',
|
||||
''
|
||||
);
|
||||
expect(result).toBe('Test World group');
|
||||
});
|
||||
|
||||
test('handles local instances', () => {
|
||||
const result = displayLocation('local:12345', 'Local World');
|
||||
expect(result).toBe('Local World');
|
||||
});
|
||||
|
||||
test('handles malformed location strings', () => {
|
||||
expect(displayLocation('invalid-location', 'Test World')).toBe(
|
||||
'Test World'
|
||||
);
|
||||
expect(displayLocation('', 'Test World')).toBe('Test World');
|
||||
expect(displayLocation(null, 'Test World')).toBe('Test World');
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration and edge cases', () => {
|
||||
test('parseLocation handles all known access types', () => {
|
||||
const testCases = [
|
||||
{ location: 'wrld_test:public', expectedAccessType: 'public' },
|
||||
{
|
||||
location: 'wrld_test:private~private(usr_123)',
|
||||
expectedAccessType: 'invite'
|
||||
},
|
||||
{
|
||||
location:
|
||||
'wrld_test:private~private(usr_123)~canRequestInvite',
|
||||
expectedAccessType: 'invite+'
|
||||
},
|
||||
{
|
||||
location: 'wrld_test:friends~friends(usr_123)',
|
||||
expectedAccessType: 'friends'
|
||||
},
|
||||
{
|
||||
location: 'wrld_test:hidden~hidden(usr_123)',
|
||||
expectedAccessType: 'friends+'
|
||||
},
|
||||
{
|
||||
location: 'wrld_test:group~group(grp_123)',
|
||||
expectedAccessType: 'group'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ location, expectedAccessType }) => {
|
||||
const result = parseLocation(location);
|
||||
expect(result.accessType).toBe(expectedAccessType);
|
||||
});
|
||||
});
|
||||
|
||||
test('displayLocation and parseLocation work together consistently', () => {
|
||||
const testCases = [
|
||||
{ location: 'offline', worldName: 'Test', expected: 'Offline' },
|
||||
{ location: 'private', worldName: 'Test', expected: 'Private' },
|
||||
{
|
||||
location: 'traveling',
|
||||
worldName: 'Test',
|
||||
expected: 'Traveling'
|
||||
},
|
||||
{
|
||||
location: 'wrld_12345',
|
||||
worldName: 'Test World',
|
||||
expected: 'Test World'
|
||||
},
|
||||
{
|
||||
location: 'wrld_12345:instance',
|
||||
worldName: 'Test World',
|
||||
expected: 'Test World public'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ location, worldName, expected }) => {
|
||||
const result = displayLocation(location, worldName);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
test('parseLocation maintains consistency across parameter order', () => {
|
||||
// Different parameter orders should produce same result
|
||||
const location1 =
|
||||
'wrld_12345:instance~region(us)~private(usr_123)~strict';
|
||||
const location2 =
|
||||
'wrld_12345:instance~strict~private(usr_123)~region(us)';
|
||||
|
||||
const result1 = parseLocation(location1);
|
||||
const result2 = parseLocation(location2);
|
||||
|
||||
expect(result1.region).toBe(result2.region);
|
||||
expect(result1.privateId).toBe(result2.privateId);
|
||||
expect(result1.strict).toBe(result2.strict);
|
||||
expect(result1.accessType).toBe(result2.accessType);
|
||||
});
|
||||
|
||||
test('parseLocation handles extremely long world IDs', () => {
|
||||
const longWorldId = 'wrld_' + 'a'.repeat(100);
|
||||
const result = parseLocation(longWorldId);
|
||||
expect(result.worldId).toBe(longWorldId);
|
||||
expect(result.isRealInstance).toBe(true);
|
||||
});
|
||||
|
||||
test('error recovery with corrupted location strings', () => {
|
||||
const corruptedCases = [
|
||||
'wrld_12345:',
|
||||
'wrld_12345:~',
|
||||
'wrld_12345:~~~',
|
||||
'wrld_12345:instance~(',
|
||||
'wrld_12345:instance~)(',
|
||||
'wrld_12345:instance~region((nested))'
|
||||
];
|
||||
|
||||
corruptedCases.forEach((location) => {
|
||||
expect(() => {
|
||||
const result = parseLocation(location);
|
||||
expect(typeof result).toBe('object');
|
||||
expect(result.worldId).toBeDefined();
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
36
src/shared/utils/base/__tests__/array.test.js
Normal file
36
src/shared/utils/base/__tests__/array.test.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { removeFromArray } from '../array';
|
||||
|
||||
describe('Array Utils', () => {
|
||||
describe('removeFromArray', () => {
|
||||
test('removes items', () => {
|
||||
const arr = [1, 2, 3];
|
||||
expect(removeFromArray(arr, 2)).toBe(true);
|
||||
expect(arr).toEqual([1, 3]);
|
||||
});
|
||||
|
||||
test('removes objects', () => {
|
||||
const obj = { id: 1 };
|
||||
const arr = [obj, { id: 2 }];
|
||||
expect(removeFromArray(arr, obj)).toBe(true);
|
||||
expect(arr).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('handles missing items', () => {
|
||||
const arr = [1, 2, 3];
|
||||
expect(removeFromArray(arr, 4)).toBe(false);
|
||||
});
|
||||
|
||||
test('removes first occurrence only', () => {
|
||||
const arr = [1, 2, 2, 3];
|
||||
removeFromArray(arr, 2);
|
||||
expect(arr).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test('handles null items', () => {
|
||||
const arr = [1, null, 2];
|
||||
// @ts-ignore
|
||||
expect(removeFromArray(arr, null)).toBe(true);
|
||||
expect(arr).toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
27
src/shared/utils/base/__tests__/format.test.js
Normal file
27
src/shared/utils/base/__tests__/format.test.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { timeToText } from '../format';
|
||||
|
||||
describe('Format Utils', () => {
|
||||
describe('timeToText', () => {
|
||||
test('formats basic durations', () => {
|
||||
expect(timeToText(1000)).toBe('1s');
|
||||
expect(timeToText(60000)).toBe('1m');
|
||||
expect(timeToText(3600000)).toBe('1h');
|
||||
});
|
||||
|
||||
test('formats with seconds flag', () => {
|
||||
expect(timeToText(60000, true)).toBe('1m 0s');
|
||||
expect(timeToText(90000, true)).toBe('1m 30s');
|
||||
});
|
||||
|
||||
test('handles zero and negative', () => {
|
||||
expect(timeToText(0)).toBe('0s');
|
||||
expect(timeToText(-1000)).toBe('1s');
|
||||
});
|
||||
|
||||
test('handles complex time', () => {
|
||||
const result = timeToText(90061000);
|
||||
expect(result).toContain('1d');
|
||||
expect(result).toContain('1h');
|
||||
});
|
||||
});
|
||||
});
|
||||
102
src/shared/utils/base/__tests__/string.test.js
Normal file
102
src/shared/utils/base/__tests__/string.test.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
escapeTag,
|
||||
escapeTagRecursive,
|
||||
textToHex,
|
||||
commaNumber,
|
||||
localeIncludes,
|
||||
changeLogRemoveLinks
|
||||
} from '../string';
|
||||
|
||||
describe('String Utils', () => {
|
||||
describe('escapeTag', () => {
|
||||
test('escapes HTML characters', () => {
|
||||
expect(escapeTag('<script>')).toBe('<script>');
|
||||
expect(escapeTag('Hello & goodbye')).toBe('Hello & goodbye');
|
||||
expect(escapeTag('"test"')).toBe('"test"');
|
||||
});
|
||||
|
||||
test('handles different types', () => {
|
||||
expect(escapeTag('')).toBe('');
|
||||
// @ts-ignore
|
||||
expect(escapeTag(null)).toBe('null');
|
||||
// @ts-ignore
|
||||
expect(escapeTag(123)).toBe('123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeTagRecursive', () => {
|
||||
test('escapes nested objects', () => {
|
||||
const input = {
|
||||
name: '<script>alert("xss")</script>',
|
||||
data: { value: 'Hello & world' }
|
||||
};
|
||||
const result = escapeTagRecursive(input);
|
||||
expect(result.name).toBe(
|
||||
'<script>alert("xss")</script>'
|
||||
);
|
||||
expect(result.data.value).toBe('Hello & world');
|
||||
});
|
||||
|
||||
test('handles arrays', () => {
|
||||
const input = ['<script>', 'normal text'];
|
||||
const result = escapeTagRecursive(input);
|
||||
expect(result[0]).toBe('<script>');
|
||||
expect(result[1]).toBe('normal text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('textToHex', () => {
|
||||
test('converts basic text', () => {
|
||||
expect(textToHex('ABC')).toBe('41 42 43');
|
||||
expect(textToHex('Hello')).toBe('48 65 6c 6c 6f');
|
||||
});
|
||||
|
||||
test('handles special cases', () => {
|
||||
expect(textToHex('')).toBe('');
|
||||
// @ts-ignore
|
||||
expect(textToHex(123)).toBe('31 32 33');
|
||||
});
|
||||
});
|
||||
|
||||
describe('commaNumber', () => {
|
||||
test('formats numbers', () => {
|
||||
expect(commaNumber(1000)).toBe('1,000');
|
||||
expect(commaNumber(1234567)).toBe('1,234,567');
|
||||
});
|
||||
|
||||
test('handles edge cases', () => {
|
||||
expect(commaNumber(0)).toBe('0');
|
||||
// @ts-ignore
|
||||
expect(commaNumber(null)).toBe('0');
|
||||
// @ts-ignore
|
||||
expect(commaNumber('abc')).toBe('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('localeIncludes', () => {
|
||||
const comparer = {
|
||||
compare: (a, b) => a.toLowerCase().localeCompare(b.toLowerCase())
|
||||
};
|
||||
|
||||
test('finds substrings', () => {
|
||||
expect(localeIncludes('Hello World', 'hello', comparer)).toBe(true);
|
||||
expect(localeIncludes('Hello World', 'xyz', comparer)).toBe(false);
|
||||
});
|
||||
|
||||
test('handles empty search', () => {
|
||||
expect(localeIncludes('Hello', '', comparer)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeLogRemoveLinks', () => {
|
||||
test('removes markdown links', () => {
|
||||
expect(
|
||||
changeLogRemoveLinks('Hello [world](http://example.com)')
|
||||
).toBe('Hello ');
|
||||
});
|
||||
|
||||
test('preserves image links', () => {
|
||||
expect(changeLogRemoveLinks('')).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -47,7 +47,11 @@ function commaNumber(num) {
|
||||
if (!num) {
|
||||
return '0';
|
||||
}
|
||||
const s = String(Number(num));
|
||||
const numValue = Number(num);
|
||||
if (isNaN(numValue)) {
|
||||
return '0';
|
||||
}
|
||||
const s = String(numValue);
|
||||
return s.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import groupRequest from '../../api/group';
|
||||
import { groupRequest } from '../../api';
|
||||
import { parseLocation } from './location';
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
*
|
||||
* @param {string} location
|
||||
* @param {string} worldName
|
||||
* @param {string} groupName
|
||||
* @param {string?} groupName
|
||||
* @returns {string}
|
||||
*/
|
||||
function displayLocation(location, worldName, groupName) {
|
||||
function displayLocation(location, worldName, groupName = '') {
|
||||
let text = worldName;
|
||||
const L = parseLocation(location);
|
||||
if (L.isOffline) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { database } from '../../service/database.js';
|
||||
import { useFriendStore } from '../../stores';
|
||||
|
||||
@@ -44,7 +43,6 @@ async function getUserMemo(userId) {
|
||||
*/
|
||||
async function saveUserMemo(id, memo) {
|
||||
const friendStore = useFriendStore();
|
||||
const { friends } = storeToRefs(friendStore);
|
||||
if (memo) {
|
||||
await database.setUserMemo({
|
||||
userId: id,
|
||||
@@ -54,7 +52,7 @@ async function saveUserMemo(id, memo) {
|
||||
} else {
|
||||
await database.deleteUserMemo(id);
|
||||
}
|
||||
const ref = friends.value.get(id);
|
||||
const ref = friendStore.friends.get(id);
|
||||
if (ref) {
|
||||
ref.memo = String(memo || '');
|
||||
if (memo) {
|
||||
@@ -71,10 +69,9 @@ async function saveUserMemo(id, memo) {
|
||||
*/
|
||||
async function getAllUserMemos() {
|
||||
const friendStore = useFriendStore();
|
||||
const { friends } = storeToRefs(friendStore);
|
||||
const memos = await database.getAllUserMemos();
|
||||
memos.forEach((memo) => {
|
||||
const ref = friends.value.get(memo.userId);
|
||||
const ref = friendStore.friends.get(memo.userId);
|
||||
if (typeof ref !== 'undefined') {
|
||||
ref.memo = memo.memo;
|
||||
ref.$nickName = '';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useAppearanceSettingsStore, useUserStore } from '../../stores';
|
||||
import { languageMappings } from '../constants';
|
||||
import { timeToText } from './base/format';
|
||||
@@ -182,15 +181,13 @@ function userImage(
|
||||
isUserDialogIcon = false
|
||||
) {
|
||||
const appAppearanceSettingsStore = useAppearanceSettingsStore();
|
||||
const { displayVRCPlusIconsAsAvatar } = storeToRefs(
|
||||
appAppearanceSettingsStore
|
||||
);
|
||||
if (!user) {
|
||||
return '';
|
||||
}
|
||||
if (
|
||||
(isUserDialogIcon && user.userIcon) ||
|
||||
(displayVRCPlusIconsAsAvatar.value && user.userIcon)
|
||||
(appAppearanceSettingsStore.displayVRCPlusIconsAsAvatar &&
|
||||
user.userIcon)
|
||||
) {
|
||||
if (isIcon) {
|
||||
return convertFileUrlToImageUrl(user.userIcon);
|
||||
@@ -238,10 +235,10 @@ function userImage(
|
||||
*/
|
||||
function userImageFull(user) {
|
||||
const appAppearanceSettingsStore = useAppearanceSettingsStore();
|
||||
const { displayVRCPlusIconsAsAvatar } = storeToRefs(
|
||||
appAppearanceSettingsStore
|
||||
);
|
||||
if (displayVRCPlusIconsAsAvatar.value && user.userIcon) {
|
||||
if (
|
||||
appAppearanceSettingsStore.displayVRCPlusIconsAsAvatar &&
|
||||
user.userIcon
|
||||
) {
|
||||
return user.userIcon;
|
||||
}
|
||||
if (user.profilePicOverride) {
|
||||
|
||||
1
src/types/globals.d.ts
vendored
1
src/types/globals.d.ts
vendored
@@ -1,4 +1,5 @@
|
||||
/// <reference types="node" />
|
||||
/// <reference types="jest" />
|
||||
|
||||
declare global {
|
||||
const WINDOWS: boolean;
|
||||
|
||||
Reference in New Issue
Block a user