use oxfmt instead of prettier

This commit is contained in:
pa
2026-03-13 22:30:12 +09:00
parent 82122a4fab
commit 7b7c1b4568
155 changed files with 3467 additions and 1631 deletions
+2 -9
View File
@@ -1,22 +1,15 @@
{ {
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 80, "printWidth": 80,
"tabWidth": 4, "tabWidth": 4,
"semi": true, "semi": true,
"singleQuote": true, "singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "none", "trailingComma": "none",
"bracketSpacing": true, "bracketSpacing": true,
"arrowParens": "always", "arrowParens": "always",
"endOfLine": "auto",
"overrides": [ "overrides": [
{ {
"files": "*.js", "files": ["*.vue"],
"options": {
"parser": "meriyah"
}
},
{
"files": "*.vue",
"options": { "options": {
"printWidth": 120, "printWidth": 120,
"bracketSameLine": true, "bracketSameLine": true,
+1 -2
View File
@@ -3,8 +3,7 @@
"vue.volar", "vue.volar",
"lokalise.i18n-ally", "lokalise.i18n-ally",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"esbenp.prettier-vscode", "oxc.oxc-vscode",
"lllllllqw.jsdoc",
"bradlc.vscode-tailwindcss" "bradlc.vscode-tailwindcss"
] ]
} }
+2 -1
View File
@@ -3,7 +3,8 @@
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"i18n-ally.sourceLanguage": "en", "i18n-ally.sourceLanguage": "en",
"i18n-ally.indent": 4, "i18n-ally.indent": 4,
"editor.defaultFormatter": "esbenp.prettier-vscode", "oxc.fmt.configPath": ".oxfmtrc.json",
"editor.defaultFormatter": "oxc.oxc-vscode",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"omnisharp.enableRoslynAnalyzers": true, "omnisharp.enableRoslynAnalyzers": true,
"omnisharp.useModernNet": false, "omnisharp.useModernNet": false,
+1 -10
View File
@@ -1,7 +1,6 @@
import { defineConfig } from 'eslint/config'; import { defineConfig } from 'eslint/config';
import { jsdoc } from 'eslint-plugin-jsdoc'; import { jsdoc } from 'eslint-plugin-jsdoc';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals'; import globals from 'globals';
import js from '@eslint/js'; import js from '@eslint/js';
import pluginVue from 'eslint-plugin-vue'; import pluginVue from 'eslint-plugin-vue';
@@ -108,7 +107,7 @@ export default defineConfig([
'jsdoc/require-returns-description': 'off', 'jsdoc/require-returns-description': 'off',
'jsdoc/reject-function-type': 'off' 'jsdoc/reject-function-type': 'off'
} }
}), })
// { // {
// ignores: [ // ignores: [
// '**/__tests__/**', // '**/__tests__/**',
@@ -127,12 +126,4 @@ export default defineConfig([
// 'pretty-import/sort-import-names': 'warn' // 'pretty-import/sort-import-names': 'warn'
// } // }
// }, // },
{
...eslintPluginPrettierRecommended,
ignores: [
'**/__tests__/**',
'**/*.spec.{js,mjs,cjs,vue}',
'**/*.test.{js,mjs,cjs,vue}'
]
}
]); ]);
+398 -115
View File
@@ -46,9 +46,7 @@
"electron-builder": "^26.8.1", "electron-builder": "^26.8.1",
"embla-carousel-vue": "^8.6.0", "embla-carousel-vue": "^8.6.0",
"eslint": "^9.39.4", "eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsdoc": "^62.8.0", "eslint-plugin-jsdoc": "^62.8.0",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-vue": "^9.33.0", "eslint-plugin-vue": "^9.33.0",
"globals": "^17.4.0", "globals": "^17.4.0",
"graphology": "^0.26.0", "graphology": "^0.26.0",
@@ -59,8 +57,8 @@
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"lucide-vue-next": "^0.562.0", "lucide-vue-next": "^0.562.0",
"noty": "^3.2.0-beta-deprecated", "noty": "^3.2.0-beta-deprecated",
"oxfmt": "^0.40.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"prettier": "^3.8.1",
"reka-ui": "^2.9.1", "reka-ui": "^2.9.1",
"remixicon": "^4.9.1", "remixicon": "^4.9.1",
"sigma": "^3.0.2", "sigma": "^3.0.2",
@@ -2243,6 +2241,353 @@
"url": "https://github.com/sponsors/Boshen" "url": "https://github.com/sponsors/Boshen"
} }
}, },
"node_modules/@oxfmt/binding-android-arm-eabi": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.40.0.tgz",
"integrity": "sha512-S6zd5r1w/HmqR8t0CTnGjFTBLDq2QKORPwriCHxo4xFNuhmOTABGjPaNvCJJVnrKBLsohOeiDX3YqQfJPF+FXw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-android-arm64": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.40.0.tgz",
"integrity": "sha512-/mbS9UUP/5Vbl2D6osIdcYiP0oie63LKMoTyGj5hyMCK/SFkl3EhtyRAfdjPvuvHC0SXdW6ePaTKkBSq1SNcIw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-darwin-arm64": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.40.0.tgz",
"integrity": "sha512-wRt8fRdfLiEhnRMBonlIbKrJWixoEmn6KCjKE9PElnrSDSXETGZfPb8ee+nQNTobXkCVvVLytp2o0obAsxl78Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-darwin-x64": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.40.0.tgz",
"integrity": "sha512-fzowhqbOE/NRy+AE5ob0+Y4X243WbWzDb00W+pKwD7d9tOqsAFbtWUwIyqqCoCLxj791m2xXIEeLH/3uz7zCCg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-freebsd-x64": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.40.0.tgz",
"integrity": "sha512-agZ9ITaqdBjcerRRFEHB8s0OyVcQW8F9ZxsszjxzeSthQ4fcN2MuOtQFWec1ed8/lDa50jSLHVE2/xPmTgtCfQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-arm-gnueabihf": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.40.0.tgz",
"integrity": "sha512-ZM2oQ47p28TP1DVIp7HL1QoMUgqlBFHey0ksHct7tMXoU5BqjNvPWw7888azzMt25lnyPODVuye1wvNbvVUFOA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-arm-musleabihf": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.40.0.tgz",
"integrity": "sha512-RBFPAxRAIsMisKM47Oe6Lwdv6agZYLz02CUhVCD1sOv5ajAcRMrnwCFBPWwGXpazToW2mjnZxFos8TuFjTU15A==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-arm64-gnu": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.40.0.tgz",
"integrity": "sha512-Nb2XbQ+wV3W2jSIihXdPj7k83eOxeSgYP3N/SRXvQ6ZYPIk6Q86qEh5Gl/7OitX3bQoQrESqm1yMLvZV8/J7dA==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-arm64-musl": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.40.0.tgz",
"integrity": "sha512-tGmWhLD/0YMotCdfezlT6tC/MJG/wKpo4vnQ3Cq+4eBk/BwNv7EmkD0VkD5F/dYkT3b8FNU01X2e8vvJuWoM1w==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-ppc64-gnu": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.40.0.tgz",
"integrity": "sha512-rVbFyM3e7YhkVnp0IVYjaSHfrBWcTRWb60LEcdNAJcE2mbhTpbqKufx0FrhWfoxOrW/+7UJonAOShoFFLigDqQ==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-riscv64-gnu": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.40.0.tgz",
"integrity": "sha512-3ZqBw14JtWeEoLiioJcXSJz8RQyPE+3jLARnYM1HdPzZG4vk+Ua8CUupt2+d+vSAvMyaQBTN2dZK+kbBS/j5mA==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-riscv64-musl": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.40.0.tgz",
"integrity": "sha512-JJ4PPSdcbGBjPvb+O7xYm2FmAsKCyuEMYhqatBAHMp/6TA6rVlf9Z/sYPa4/3Bommb+8nndm15SPFRHEPU5qFA==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-s390x-gnu": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.40.0.tgz",
"integrity": "sha512-Kp0zNJoX9Ik77wUya2tpBY3W9f40VUoMQLWVaob5SgCrblH/t2xr/9B2bWHfs0WCefuGmqXcB+t0Lq77sbBmZw==",
"cpu": [
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-x64-gnu": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.40.0.tgz",
"integrity": "sha512-7YTCNzleWTaQTqNGUNQ66qVjpoV6DjbCOea+RnpMBly2bpzrI/uu7Rr+2zcgRfNxyjXaFTVQKaRKjqVdeUfeVA==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-x64-musl": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.40.0.tgz",
"integrity": "sha512-hWnSzJ0oegeOwfOEeejYXfBqmnRGHusgtHfCPzmvJvHTwy1s3Neo59UKc1CmpE3zxvrCzJoVHos0rr97GHMNPw==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-openharmony-arm64": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.40.0.tgz",
"integrity": "sha512-28sJC1lR4qtBJGzSRRbPnSW3GxU2+4YyQFE6rCmsUYqZ5XYH8jg0/w+CvEzQ8TuAQz5zLkcA25nFQGwoU0PT3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-win32-arm64-msvc": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.40.0.tgz",
"integrity": "sha512-cDkRnyT0dqwF5oIX1Cv59HKCeZQFbWWdUpXa3uvnHFT2iwYSSZspkhgjXjU6iDp5pFPaAEAe9FIbMoTgkTmKPg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-win32-ia32-msvc": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.40.0.tgz",
"integrity": "sha512-7rPemBJjqm5Gkv6ZRCPvK8lE6AqQ/2z31DRdWazyx2ZvaSgL7QGofHXHNouRpPvNsT9yxRNQJgigsWkc+0qg4w==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-win32-x64-msvc": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.40.0.tgz",
"integrity": "sha512-/Zmj0yTYSvmha6TG1QnoLqVT7ZMRDqXvFXXBQpIjteEwx9qvUYMBH2xbiOFhDeMUJkGwC3D6fdKsFtaqUvkwNA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@pinia/testing": { "node_modules/@pinia/testing": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@pinia/testing/-/testing-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@pinia/testing/-/testing-1.0.3.tgz",
@@ -2267,19 +2612,6 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@pkgr/core": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/pkgr"
}
},
"node_modules/@preact/signals-core": { "node_modules/@preact/signals-core": {
"version": "1.13.0", "version": "1.13.0",
"resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.13.0.tgz", "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.13.0.tgz",
@@ -6658,22 +6990,6 @@
} }
} }
}, },
"node_modules/eslint-config-prettier": {
"version": "10.1.8",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
"funding": {
"url": "https://opencollective.com/eslint-config-prettier"
},
"peerDependencies": {
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-plugin-jsdoc": { "node_modules/eslint-plugin-jsdoc": {
"version": "62.8.0", "version": "62.8.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-62.8.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-62.8.0.tgz",
@@ -6734,37 +7050,6 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/eslint-plugin-prettier": {
"version": "5.5.5",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz",
"integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"prettier-linter-helpers": "^1.0.1",
"synckit": "^0.11.12"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint-plugin-prettier"
},
"peerDependencies": {
"@types/eslint": ">=8.0.0",
"eslint": ">=8.0.0",
"eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
"prettier": ">=3.0.0"
},
"peerDependenciesMeta": {
"@types/eslint": {
"optional": true
},
"eslint-config-prettier": {
"optional": true
}
}
},
"node_modules/eslint-plugin-vue": { "node_modules/eslint-plugin-vue": {
"version": "9.33.0", "version": "9.33.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz",
@@ -6996,13 +7281,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-diff": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/fast-json-stable-stringify": { "node_modules/fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -9465,6 +9743,46 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/oxfmt": {
"version": "0.40.0",
"resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.40.0.tgz",
"integrity": "sha512-g0C3I7xUj4b4DcagevM9kgH6+pUHytikxUcn3/VUkvzTNaaXBeyZqb7IBsHwojeXm4mTBEC/aBjBTMVUkZwWUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinypool": "2.1.0"
},
"bin": {
"oxfmt": "bin/oxfmt"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/sponsors/Boshen"
},
"optionalDependencies": {
"@oxfmt/binding-android-arm-eabi": "0.40.0",
"@oxfmt/binding-android-arm64": "0.40.0",
"@oxfmt/binding-darwin-arm64": "0.40.0",
"@oxfmt/binding-darwin-x64": "0.40.0",
"@oxfmt/binding-freebsd-x64": "0.40.0",
"@oxfmt/binding-linux-arm-gnueabihf": "0.40.0",
"@oxfmt/binding-linux-arm-musleabihf": "0.40.0",
"@oxfmt/binding-linux-arm64-gnu": "0.40.0",
"@oxfmt/binding-linux-arm64-musl": "0.40.0",
"@oxfmt/binding-linux-ppc64-gnu": "0.40.0",
"@oxfmt/binding-linux-riscv64-gnu": "0.40.0",
"@oxfmt/binding-linux-riscv64-musl": "0.40.0",
"@oxfmt/binding-linux-s390x-gnu": "0.40.0",
"@oxfmt/binding-linux-x64-gnu": "0.40.0",
"@oxfmt/binding-linux-x64-musl": "0.40.0",
"@oxfmt/binding-openharmony-arm64": "0.40.0",
"@oxfmt/binding-win32-arm64-msvc": "0.40.0",
"@oxfmt/binding-win32-ia32-msvc": "0.40.0",
"@oxfmt/binding-win32-x64-msvc": "0.40.0"
}
},
"node_modules/p-cancelable": { "node_modules/p-cancelable": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
@@ -9821,35 +10139,6 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/prettier": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-linter-helpers": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz",
"integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-diff": "^1.1.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/proc-log": { "node_modules/proc-log": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz",
@@ -10809,22 +11098,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/synckit": {
"version": "0.11.12",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz",
"integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@pkgr/core": "^0.2.9"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/synckit"
}
},
"node_modules/tailwind-merge": { "node_modules/tailwind-merge": {
"version": "3.5.0", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
@@ -11033,6 +11306,16 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/tinypool": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz",
"integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^20.0.0 || >=22.0.0"
}
},
"node_modules/tinyrainbow": { "node_modules/tinyrainbow": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+1 -3
View File
@@ -65,9 +65,7 @@
"electron-builder": "^26.8.1", "electron-builder": "^26.8.1",
"embla-carousel-vue": "^8.6.0", "embla-carousel-vue": "^8.6.0",
"eslint": "^9.39.4", "eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsdoc": "^62.8.0", "eslint-plugin-jsdoc": "^62.8.0",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-vue": "^9.33.0", "eslint-plugin-vue": "^9.33.0",
"globals": "^17.4.0", "globals": "^17.4.0",
"graphology": "^0.26.0", "graphology": "^0.26.0",
@@ -78,8 +76,8 @@
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"lucide-vue-next": "^0.562.0", "lucide-vue-next": "^0.562.0",
"noty": "^3.2.0-beta-deprecated", "noty": "^3.2.0-beta-deprecated",
"oxfmt": "^0.40.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"prettier": "^3.8.1",
"reka-ui": "^2.9.1", "reka-ui": "^2.9.1",
"remixicon": "^4.9.1", "remixicon": "^4.9.1",
"sigma": "^3.0.2", "sigma": "^3.0.2",
+3 -2
View File
@@ -1,5 +1,6 @@
const { contextBridge, ipcRenderer } = require('electron'); const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electronAPI', {
onUpdateImage: (callback) => ipcRenderer.on('update-image', (event, base64) => callback(base64)) onUpdateImage: (callback) =>
}); ipcRenderer.on('update-image', (event, base64) => callback(base64))
});
+4 -2
View File
@@ -3,7 +3,10 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
const mockRequest = vi.fn(); const mockRequest = vi.fn();
const mockPatchAndRefetchActiveQuery = vi.fn(() => Promise.resolve()); const mockPatchAndRefetchActiveQuery = vi.fn(() => Promise.resolve());
const mockApplyCurrentUser = vi.fn((json) => ({ id: json.id || 'usr_me', ...json })); const mockApplyCurrentUser = vi.fn((json) => ({
id: json.id || 'usr_me',
...json
}));
const mockApplyUser = vi.fn((json) => ({ ...json })); const mockApplyUser = vi.fn((json) => ({ ...json }));
const mockApplyWorld = vi.fn((json) => ({ ...json })); const mockApplyWorld = vi.fn((json) => ({ ...json }));
@@ -91,5 +94,4 @@ describe('entity mutation query sync', () => {
}) })
); );
}); });
}); });
+5 -3
View File
@@ -19,8 +19,7 @@ vi.mock('../../stores', () => ({
vi.mock('../../coordinators/favoriteCoordinator', () => ({ vi.mock('../../coordinators/favoriteCoordinator', () => ({
handleFavoriteAdd: (...args) => mockHandleFavoriteAdd(...args), handleFavoriteAdd: (...args) => mockHandleFavoriteAdd(...args),
handleFavoriteDelete: (...args) => mockHandleFavoriteDelete(...args), handleFavoriteDelete: (...args) => mockHandleFavoriteDelete(...args),
handleFavoriteGroupClear: (...args) => handleFavoriteGroupClear: (...args) => mockHandleFavoriteGroupClear(...args)
mockHandleFavoriteGroupClear(...args)
})); }));
vi.mock('../../queries', () => ({ vi.mock('../../queries', () => ({
@@ -39,7 +38,10 @@ describe('favorite query sync', () => {
test('favorite mutations invalidate active favorite queries', async () => { test('favorite mutations invalidate active favorite queries', async () => {
mockRequest.mockResolvedValue({ ok: true }); mockRequest.mockResolvedValue({ ok: true });
await favoriteRequest.addFavorite({ type: 'world', favoriteId: 'wrld_1' }); await favoriteRequest.addFavorite({
type: 'world',
favoriteId: 'wrld_1'
});
await favoriteRequest.deleteFavorite({ objectId: 'fav_1' }); await favoriteRequest.deleteFavorite({ objectId: 'fav_1' });
await favoriteRequest.saveFavoriteGroup({ await favoriteRequest.saveFavoriteGroup({
type: 'world', type: 'world',
+6 -1
View File
@@ -24,7 +24,12 @@ vi.mock('../../queries', () => ({
prints: (params) => ['gallery', 'prints', params], prints: (params) => ['gallery', 'prints', params],
print: (printId) => ['gallery', 'print', printId], print: (printId) => ['gallery', 'print', printId],
inventoryItems: (params) => ['inventory', 'items', params], inventoryItems: (params) => ['inventory', 'items', params],
userInventoryItem: (params) => ['inventory', 'item', params.userId, params.inventoryId], userInventoryItem: (params) => [
'inventory',
'item',
params.userId,
params.inventoryId
],
file: (fileId) => ['file', fileId] file: (fileId) => ['file', fileId]
} }
})); }));
+190 -32
View File
@@ -10,28 +10,138 @@ vi.mock('../../queries', () => ({
invalidateQueries: vi.fn().mockResolvedValue(undefined) invalidateQueries: vi.fn().mockResolvedValue(undefined)
}, },
entityQueryPolicies: { entityQueryPolicies: {
user: { staleTime: 20000, gcTime: 90000, retry: 1, refetchOnWindowFocus: false }, user: {
worldCollection: { staleTime: 60000, gcTime: 300000, retry: 1, refetchOnWindowFocus: false }, staleTime: 20000,
groupCollection: { staleTime: 60000, gcTime: 300000, retry: 1, refetchOnWindowFocus: false }, gcTime: 90000,
groupCalendarCollection: { staleTime: 120000, gcTime: 600000, retry: 1, refetchOnWindowFocus: false }, retry: 1,
groupFollowingCalendarCollection: { staleTime: 60000, gcTime: 300000, retry: 1, refetchOnWindowFocus: false }, refetchOnWindowFocus: false
groupFeaturedCalendarCollection: { staleTime: 300000, gcTime: 900000, retry: 1, refetchOnWindowFocus: false }, },
groupCalendarEvent: { staleTime: 120000, gcTime: 600000, retry: 1, refetchOnWindowFocus: false }, worldCollection: {
avatar: { staleTime: 60000, gcTime: 300000, retry: 1, refetchOnWindowFocus: false }, staleTime: 60000,
avatarCollection: { staleTime: 60000, gcTime: 300000, retry: 1, refetchOnWindowFocus: false }, gcTime: 300000,
avatarGallery: { staleTime: 30000, gcTime: 120000, retry: 1, refetchOnWindowFocus: false }, retry: 1,
world: { staleTime: 60000, gcTime: 300000, retry: 1, refetchOnWindowFocus: false }, refetchOnWindowFocus: false
group: { staleTime: 60000, gcTime: 300000, retry: 1, refetchOnWindowFocus: false }, },
friendList: { staleTime: 20000, gcTime: 90000, retry: 1, refetchOnWindowFocus: false }, groupCollection: {
favoriteCollection: { staleTime: 60000, gcTime: 300000, retry: 1, refetchOnWindowFocus: false }, staleTime: 60000,
galleryCollection: { staleTime: 60000, gcTime: 300000, retry: 1, refetchOnWindowFocus: false }, gcTime: 300000,
inventoryCollection: { staleTime: 20000, gcTime: 120000, retry: 1, refetchOnWindowFocus: false }, retry: 1,
inventoryObject: { staleTime: 60000, gcTime: 300000, retry: 1, refetchOnWindowFocus: false }, refetchOnWindowFocus: false
fileAnalysis: { staleTime: 120000, gcTime: 600000, retry: 1, refetchOnWindowFocus: false }, },
worldPersistData: { staleTime: 120000, gcTime: 600000, retry: 1, refetchOnWindowFocus: false }, groupCalendarCollection: {
mutualCounts: { staleTime: 120000, gcTime: 600000, retry: 1, refetchOnWindowFocus: false }, staleTime: 120000,
visits: { staleTime: 300000, gcTime: 900000, retry: 1, refetchOnWindowFocus: false }, gcTime: 600000,
fileObject: { staleTime: 60000, gcTime: 300000, retry: 1, refetchOnWindowFocus: false } retry: 1,
refetchOnWindowFocus: false
},
groupFollowingCalendarCollection: {
staleTime: 60000,
gcTime: 300000,
retry: 1,
refetchOnWindowFocus: false
},
groupFeaturedCalendarCollection: {
staleTime: 300000,
gcTime: 900000,
retry: 1,
refetchOnWindowFocus: false
},
groupCalendarEvent: {
staleTime: 120000,
gcTime: 600000,
retry: 1,
refetchOnWindowFocus: false
},
avatar: {
staleTime: 60000,
gcTime: 300000,
retry: 1,
refetchOnWindowFocus: false
},
avatarCollection: {
staleTime: 60000,
gcTime: 300000,
retry: 1,
refetchOnWindowFocus: false
},
avatarGallery: {
staleTime: 30000,
gcTime: 120000,
retry: 1,
refetchOnWindowFocus: false
},
world: {
staleTime: 60000,
gcTime: 300000,
retry: 1,
refetchOnWindowFocus: false
},
group: {
staleTime: 60000,
gcTime: 300000,
retry: 1,
refetchOnWindowFocus: false
},
friendList: {
staleTime: 20000,
gcTime: 90000,
retry: 1,
refetchOnWindowFocus: false
},
favoriteCollection: {
staleTime: 60000,
gcTime: 300000,
retry: 1,
refetchOnWindowFocus: false
},
galleryCollection: {
staleTime: 60000,
gcTime: 300000,
retry: 1,
refetchOnWindowFocus: false
},
inventoryCollection: {
staleTime: 20000,
gcTime: 120000,
retry: 1,
refetchOnWindowFocus: false
},
inventoryObject: {
staleTime: 60000,
gcTime: 300000,
retry: 1,
refetchOnWindowFocus: false
},
fileAnalysis: {
staleTime: 120000,
gcTime: 600000,
retry: 1,
refetchOnWindowFocus: false
},
worldPersistData: {
staleTime: 120000,
gcTime: 600000,
retry: 1,
refetchOnWindowFocus: false
},
mutualCounts: {
staleTime: 120000,
gcTime: 600000,
retry: 1,
refetchOnWindowFocus: false
},
visits: {
staleTime: 300000,
gcTime: 900000,
retry: 1,
refetchOnWindowFocus: false
},
fileObject: {
staleTime: 60000,
gcTime: 300000,
retry: 1,
refetchOnWindowFocus: false
}
}, },
fetchWithEntityPolicy: (...args) => mockFetchWithEntityPolicy(...args), fetchWithEntityPolicy: (...args) => mockFetchWithEntityPolicy(...args),
queryKeys: { queryKeys: {
@@ -40,16 +150,46 @@ vi.mock('../../queries', () => ({
worldsByUser: (params) => ['worlds', 'user', params.userId, params], worldsByUser: (params) => ['worlds', 'user', params.userId, params],
groupCalendar: (groupId) => ['group', groupId, 'calendar'], groupCalendar: (groupId) => ['group', groupId, 'calendar'],
groupCalendars: (params) => ['group', 'calendar', params], groupCalendars: (params) => ['group', 'calendar', params],
followingGroupCalendars: (params) => ['group', 'calendar', 'following', params], followingGroupCalendars: (params) => [
featuredGroupCalendars: (params) => ['group', 'calendar', 'featured', params], 'group',
'calendar',
'following',
params
],
featuredGroupCalendars: (params) => [
'group',
'calendar',
'featured',
params
],
avatar: (avatarId) => ['avatar', avatarId], avatar: (avatarId) => ['avatar', avatarId],
world: (worldId) => ['world', worldId], world: (worldId) => ['world', worldId],
group: (groupId, includeRoles) => ['group', groupId, Boolean(includeRoles)], group: (groupId, includeRoles) => [
'group',
groupId,
Boolean(includeRoles)
],
groupPosts: (params) => ['group', params.groupId, 'posts', params], groupPosts: (params) => ['group', params.groupId, 'posts', params],
groupMember: (params) => ['group', params.groupId, 'member', params.userId], groupMember: (params) => [
'group',
params.groupId,
'member',
params.userId
],
groupMembers: (params) => ['group', params.groupId, 'members', params], groupMembers: (params) => ['group', params.groupId, 'members', params],
groupGallery: (params) => ['group', params.groupId, 'gallery', params.galleryId, params], groupGallery: (params) => [
groupCalendarEvent: (params) => ['group', params.groupId, 'calendarEvent', params.eventId], 'group',
params.groupId,
'gallery',
params.galleryId,
params
],
groupCalendarEvent: (params) => [
'group',
params.groupId,
'calendarEvent',
params.eventId
],
avatarGallery: (avatarId) => ['avatar', avatarId, 'gallery'], avatarGallery: (avatarId) => ['avatar', avatarId, 'gallery'],
friends: (params) => ['friends', params], friends: (params) => ['friends', params],
favoriteLimits: () => ['favorite', 'limits'], favoriteLimits: () => ['favorite', 'limits'],
@@ -61,10 +201,24 @@ vi.mock('../../queries', () => ({
prints: (params) => ['gallery', 'prints', params], prints: (params) => ['gallery', 'prints', params],
print: (printId) => ['gallery', 'print', printId], print: (printId) => ['gallery', 'print', printId],
inventoryItem: (inventoryId) => ['inventory', 'item', inventoryId], inventoryItem: (inventoryId) => ['inventory', 'item', inventoryId],
userInventoryItem: (params) => ['inventory', 'item', params.userId, params.inventoryId], userInventoryItem: (params) => [
'inventory',
'item',
params.userId,
params.inventoryId
],
inventoryItems: (params) => ['inventory', 'items', params], inventoryItems: (params) => ['inventory', 'items', params],
inventoryTemplate: (inventoryTemplateId) => ['inventory', 'template', inventoryTemplateId], inventoryTemplate: (inventoryTemplateId) => [
fileAnalysis: (params) => ['analysis', params.fileId, Number(params.version), String(params.variant || '')], 'inventory',
'template',
inventoryTemplateId
],
fileAnalysis: (params) => [
'analysis',
params.fileId,
Number(params.version),
String(params.variant || '')
],
worldPersistData: (worldId) => ['world', worldId, 'persistData'], worldPersistData: (worldId) => ['world', worldId, 'persistData'],
mutualCounts: (userId) => ['user', userId, 'mutualCounts'], mutualCounts: (userId) => ['user', userId, 'mutualCounts'],
visits: () => ['visits'], visits: () => ['visits'],
@@ -102,7 +256,11 @@ vi.mock('../group', () => ({
})); }));
vi.mock('../avatar', () => ({ vi.mock('../avatar', () => ({
default: { getAvatar: vi.fn(), getAvatarGallery: vi.fn(), getAvatars: vi.fn() } default: {
getAvatar: vi.fn(),
getAvatarGallery: vi.fn(),
getAvatars: vi.fn()
}
})); }));
vi.mock('../friend', () => ({ default: { getFriends: vi.fn() } })); vi.mock('../friend', () => ({ default: { getFriends: vi.fn() } }));
vi.mock('../favorite', () => ({ vi.mock('../favorite', () => ({
+2 -9
View File
@@ -43,12 +43,7 @@
resolveRegion, resolveRegion,
translateAccessType translateAccessType
} from '../shared/utils'; } from '../shared/utils';
import { import { useAppearanceSettingsStore, useInstanceStore, useSearchStore, useWorldStore } from '../stores';
useAppearanceSettingsStore,
useInstanceStore,
useSearchStore,
useWorldStore
} from '../stores';
import { showGroupDialog } from '../coordinators/groupCoordinator'; import { showGroupDialog } from '../coordinators/groupCoordinator';
import { showWorldDialog } from '../coordinators/worldCoordinator'; import { showWorldDialog } from '../coordinators/worldCoordinator';
import { Spinner } from './ui/spinner'; import { Spinner } from './ui/spinner';
@@ -104,9 +99,7 @@
} }
]); ]);
const tooltipContent = computed(() => `${t('dialog.new_instance.instance_id')}: #${instanceName.value}`); const tooltipContent = computed(() => `${t('dialog.new_instance.instance_id')}: #${instanceName.value}`);
const tooltipDisabled = computed( const tooltipDisabled = computed(() => props.disableTooltip || !instanceName.value || showInstanceIdInLocation.value);
() => props.disableTooltip || !instanceName.value || showInstanceIdInLocation.value
);
const closedTooltip = computed(() => t('dialog.user.info.instance_closed')); const closedTooltip = computed(() => t('dialog.user.info.instance_closed'));
let isDisposed = false; let isDisposed = false;
+3 -1
View File
@@ -216,7 +216,9 @@ describe('AvatarInfo.vue', () => {
test('does not call showAvatarAuthorDialog when no imageurl', async () => { test('does not call showAvatarAuthorDialog when no imageurl', async () => {
const wrapper = mountAvatarInfo({}); const wrapper = mountAvatarInfo({});
await wrapper.trigger('click'); await wrapper.trigger('click');
expect(avatarCoordinatorModule.showAvatarAuthorDialog).not.toHaveBeenCalled(); expect(
avatarCoordinatorModule.showAvatarAuthorDialog
).not.toHaveBeenCalled();
}); });
}); });
}); });
+14 -4
View File
@@ -11,7 +11,8 @@ vi.mock('@/components/ui/tooltip', () => ({
vi.mock('@/components/ui/button', () => ({ vi.mock('@/components/ui/button', () => ({
Button: { Button: {
emits: ['click'], emits: ['click'],
template: '<button data-testid="back-btn" @click="$emit(\'click\', $event)"><slot /></button>' template:
'<button data-testid="back-btn" @click="$emit(\'click\', $event)"><slot /></button>'
} }
})); }));
@@ -58,7 +59,10 @@ describe('BackToTop.vue', () => {
await btn.trigger('click'); await btn.trigger('click');
expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' }); expect(window.scrollTo).toHaveBeenCalledWith({
top: 0,
behavior: 'smooth'
});
}); });
it('uses virtualizer scrollToIndex when provided', async () => { it('uses virtualizer scrollToIndex when provided', async () => {
@@ -78,7 +82,10 @@ describe('BackToTop.vue', () => {
const btn = wrapper.get('[data-testid="back-btn"]'); const btn = wrapper.get('[data-testid="back-btn"]');
await btn.trigger('click'); await btn.trigger('click');
expect(scrollToIndex).toHaveBeenCalledWith(0, { align: 'start', behavior: 'auto' }); expect(scrollToIndex).toHaveBeenCalledWith(0, {
align: 'start',
behavior: 'auto'
});
expect(window.scrollTo).not.toHaveBeenCalled(); expect(window.scrollTo).not.toHaveBeenCalled();
}); });
@@ -103,6 +110,9 @@ describe('BackToTop.vue', () => {
const btn = wrapper.get('[data-testid="back-btn"]'); const btn = wrapper.get('[data-testid="back-btn"]');
await btn.trigger('click'); await btn.trigger('click');
expect(target.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'auto' }); expect(target.scrollTo).toHaveBeenCalledWith({
top: 0,
behavior: 'auto'
});
}); });
}); });
@@ -24,7 +24,9 @@ describe('CountdownTimer.vue', () => {
mocks.setInterval.mockClear(); mocks.setInterval.mockClear();
mocks.clearInterval.mockClear(); mocks.clearInterval.mockClear();
mocks.timeToText.mockClear(); mocks.timeToText.mockClear();
vi.spyOn(Date, 'now').mockReturnValue(new Date('2026-01-01T00:00:00.000Z').getTime()); vi.spyOn(Date, 'now').mockReturnValue(
new Date('2026-01-01T00:00:00.000Z').getTime()
);
}); });
afterEach(() => { afterEach(() => {
@@ -55,7 +57,10 @@ describe('CountdownTimer.vue', () => {
expect(wrapper.text()).toBe('-'); expect(wrapper.text()).toBe('-');
await wrapper.setProps({ datetime: '2025-12-31T23:59:30.000Z', hours: 0 }); await wrapper.setProps({
datetime: '2025-12-31T23:59:30.000Z',
hours: 0
});
await nextTick(); await nextTick();
expect(wrapper.text()).toBe('-'); expect(wrapper.text()).toBe('-');
}); });
@@ -22,7 +22,8 @@ describe('DeprecationAlert.vue', () => {
global: { global: {
stubs: { stubs: {
i18nT: { i18nT: {
template: '<span data-testid="i18n-t"><slot name="feature" /></span>' template:
'<span data-testid="i18n-t"><slot name="feature" /></span>'
} }
} }
} }
+6 -2
View File
@@ -2,7 +2,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
const mocks = vi.hoisted(() => ({ const mocks = vi.hoisted(() => ({
fetch: vi.fn(() => Promise.resolve({ json: { displayName: 'Fetched User' } })), fetch: vi.fn(() =>
Promise.resolve({ json: { displayName: 'Fetched User' } })
),
showUserDialog: vi.fn() showUserDialog: vi.fn()
})); }));
@@ -52,7 +54,9 @@ describe('DisplayName.vue', () => {
await flush(); await flush();
expect(mocks.fetch).toHaveBeenCalledWith('user.dialog', { userId: 'usr_2' }); expect(mocks.fetch).toHaveBeenCalledWith('user.dialog', {
userId: 'usr_2'
});
expect(wrapper.text()).toBe('Fetched User'); expect(wrapper.text()).toBe('Fetched User');
}); });
+13 -4
View File
@@ -27,8 +27,13 @@ vi.mock('../../shared/utils', () => ({
vi.mock('../ui/avatar', () => ({ vi.mock('../ui/avatar', () => ({
Avatar: { template: '<div data-testid="avatar"><slot /></div>' }, Avatar: { template: '<div data-testid="avatar"><slot /></div>' },
AvatarImage: { props: ['src'], template: '<img data-testid="avatar-image" :src="src" />' }, AvatarImage: {
AvatarFallback: { template: '<span data-testid="avatar-fallback"><slot /></span>' } props: ['src'],
template: '<img data-testid="avatar-image" :src="src" />'
},
AvatarFallback: {
template: '<span data-testid="avatar-fallback"><slot /></span>'
}
})); }));
vi.mock('lucide-vue-next', () => ({ vi.mock('lucide-vue-next', () => ({
@@ -87,8 +92,12 @@ describe('Emoji.vue', () => {
expect(mocks.getCachedEmoji).toHaveBeenCalledWith('file_1'); expect(mocks.getCachedEmoji).toHaveBeenCalledWith('file_1');
expect(wrapper.find('[data-testid="avatar"]').exists()).toBe(true); expect(wrapper.find('[data-testid="avatar"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="avatar-image"]').attributes('src')).toBe('https://example.com/file_2.png'); expect(
expect(wrapper.find('[data-testid="avatar-fallback"]').exists()).toBe(true); wrapper.find('[data-testid="avatar-image"]').attributes('src')
).toBe('https://example.com/file_2.png');
expect(wrapper.find('[data-testid="avatar-fallback"]').exists()).toBe(
true
);
}); });
it('updates when imageUrl changes', async () => { it('updates when imageUrl changes', async () => {
@@ -2,20 +2,66 @@ import { describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
const mocks = vi.hoisted(() => ({ const mocks = vi.hoisted(() => ({
dialog: { value: { visible: true, imageUrl: 'https://example.com/a.png', fileName: 'a.png' } } dialog: {
value: {
visible: true,
imageUrl: 'https://example.com/a.png',
fileName: 'a.png'
}
}
})); }));
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s })); vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) })); vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('@/stores/settings/general', () => ({ useGeneralSettingsStore: () => ({ disableGpuAcceleration: { value: false } }) })); vi.mock('@/stores/settings/general', () => ({
vi.mock('../../stores', () => ({ useGalleryStore: () => ({ fullscreenImageDialog: mocks.dialog, showFullscreenImageDialog: vi.fn() }) })); useGeneralSettingsStore: () => ({
vi.mock('@/lib/modalPortalLayers', () => ({ acquireModalPortalLayer: () => ({ element: 'body', bringToFront: vi.fn(), release: vi.fn() }) })); disableGpuAcceleration: { value: false }
})
}));
vi.mock('../../stores', () => ({
useGalleryStore: () => ({
fullscreenImageDialog: mocks.dialog,
showFullscreenImageDialog: vi.fn()
})
}));
vi.mock('@/lib/modalPortalLayers', () => ({
acquireModalPortalLayer: () => ({
element: 'body',
bringToFront: vi.fn(),
release: vi.fn()
})
}));
vi.mock('@/lib/utils', () => ({ cn: (...a) => a.filter(Boolean).join(' ') })); vi.mock('@/lib/utils', () => ({ cn: (...a) => a.filter(Boolean).join(' ') }));
vi.mock('../../shared/utils', () => ({ escapeTag: (s) => s, extractFileId: () => 'f1' })); vi.mock('../../shared/utils', () => ({
vi.mock('vue-sonner', () => ({ toast: { info: vi.fn(() => 'id'), success: vi.fn(), error: vi.fn(), dismiss: vi.fn() } })); escapeTag: (s) => s,
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' } })); extractFileId: () => 'f1'
vi.mock('reka-ui', () => ({ DialogPortal: { template: '<div><slot /></div>' }, DialogOverlay: { template: '<div><slot /></div>' }, DialogContent: { emits: ['click'], template: '<div @click="$emit(\'click\')"><slot /></div>' } })); }));
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button :aria-label="$attrs[\'aria-label\']" @click="$emit(\'click\')"><slot /></button>' } })); vi.mock('vue-sonner', () => ({
toast: {
info: vi.fn(() => 'id'),
success: vi.fn(),
error: vi.fn(),
dismiss: vi.fn()
}
}));
vi.mock('@/components/ui/dialog', () => ({
Dialog: { template: '<div><slot /></div>' }
}));
vi.mock('reka-ui', () => ({
DialogPortal: { template: '<div><slot /></div>' },
DialogOverlay: { template: '<div><slot /></div>' },
DialogContent: {
emits: ['click'],
template: '<div @click="$emit(\'click\')"><slot /></div>'
}
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button :aria-label="$attrs[\'aria-label\']" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('lucide-vue-next', () => ({ vi.mock('lucide-vue-next', () => ({
Copy: { template: '<i />' }, Copy: { template: '<i />' },
Download: { template: '<i />' }, Download: { template: '<i />' },
@@ -33,17 +33,35 @@ vi.mock('../../stores/globalSearch', () => ({
selectResult: (...args) => mocks.selectResult(...args) selectResult: (...args) => mocks.selectResult(...args)
}) })
})); }));
vi.mock('../../composables/useUserDisplay', () => ({ useUserDisplay: () => ({ userImage: (...a) => mocks.userImage(...a) }) })); vi.mock('../../composables/useUserDisplay', () => ({
vi.mock('../GlobalSearchSync.vue', () => ({ default: { template: '<div data-testid="sync" />' } })); useUserDisplay: () => ({ userImage: (...a) => mocks.userImage(...a) })
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' }, DialogHeader: { template: '<div><slot /></div>' }, DialogTitle: { template: '<div><slot /></div>' }, DialogDescription: { template: '<div><slot /></div>' } })); }));
vi.mock('../GlobalSearchSync.vue', () => ({
default: { template: '<div data-testid="sync" />' }
}));
vi.mock('@/components/ui/dialog', () => ({
Dialog: { template: '<div><slot /></div>' },
DialogContent: { template: '<div><slot /></div>' },
DialogHeader: { template: '<div><slot /></div>' },
DialogTitle: { template: '<div><slot /></div>' },
DialogDescription: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/command', () => ({ vi.mock('@/components/ui/command', () => ({
Command: { template: '<div><slot /></div>' }, Command: { template: '<div><slot /></div>' },
CommandInput: { template: '<input />' }, CommandInput: { template: '<input />' },
CommandList: { template: '<div><slot /></div>' }, CommandList: { template: '<div><slot /></div>' },
CommandGroup: { template: '<div><slot /></div>' }, CommandGroup: { template: '<div><slot /></div>' },
CommandItem: { emits: ['select'], template: '<button data-testid="cmd-item" @click="$emit(\'select\')"><slot /></button>' } CommandItem: {
emits: ['select'],
template:
'<button data-testid="cmd-item" @click="$emit(\'select\')"><slot /></button>'
}
}));
vi.mock('lucide-vue-next', () => ({
Globe: { template: '<i />' },
Image: { template: '<i />' },
Users: { template: '<i />' }
})); }));
vi.mock('lucide-vue-next', () => ({ Globe: { template: '<i />' }, Image: { template: '<i />' }, Users: { template: '<i />' } }));
import GlobalSearchDialog from '../GlobalSearchDialog.vue'; import GlobalSearchDialog from '../GlobalSearchDialog.vue';
@@ -12,7 +12,10 @@ const mocks = vi.hoisted(() => ({
} }
}, },
filterState: null, filterState: null,
allItemsEntries: [['a', {}], ['b', {}]], allItemsEntries: [
['a', {}],
['b', {}]
],
allGroupsEntries: [['g1', {}]] allGroupsEntries: [['g1', {}]]
})); }));
@@ -4,11 +4,18 @@ import { nextTick } from 'vue';
const mocks = vi.hoisted(() => ({ const mocks = vi.hoisted(() => ({
checkCanInviteSelf: vi.fn(() => true), checkCanInviteSelf: vi.fn(() => true),
parseLocation: vi.fn(() => ({ isRealInstance: true, instanceId: 'inst_1', worldId: 'wrld_1', tag: 'wrld_1:inst_1' })), parseLocation: vi.fn(() => ({
isRealInstance: true,
instanceId: 'inst_1',
worldId: 'wrld_1',
tag: 'wrld_1:inst_1'
})),
hasGroupPermission: vi.fn(() => false), hasGroupPermission: vi.fn(() => false),
formatDateFilter: vi.fn(() => 'formatted-date'), formatDateFilter: vi.fn(() => 'formatted-date'),
selfInvite: vi.fn(() => Promise.resolve({})), selfInvite: vi.fn(() => Promise.resolve({})),
closeInstance: vi.fn(() => Promise.resolve({ json: { id: 'inst_closed' } })), closeInstance: vi.fn(() =>
Promise.resolve({ json: { id: 'inst_closed' } })
),
showUserDialog: vi.fn(), showUserDialog: vi.fn(),
toastSuccess: vi.fn(), toastSuccess: vi.fn(),
applyInstance: vi.fn(), applyInstance: vi.fn(),
@@ -18,7 +25,10 @@ const mocks = vi.hoisted(() => ({
instanceJoinHistory: { value: new Map() }, instanceJoinHistory: { value: new Map() },
canOpenInstanceInGame: false, canOpenInstanceInGame: false,
isOpeningInstance: false, isOpeningInstance: false,
lastLocation: { location: 'wrld_here:111', playerList: new Set(['u1', 'u2']) }, lastLocation: {
location: 'wrld_here:111',
playerList: new Set(['u1', 'u2'])
},
currentUser: { id: 'usr_me' }, currentUser: { id: 'usr_me' },
cachedGroups: new Map() cachedGroups: new Map()
})); }));
@@ -31,7 +41,9 @@ vi.mock('pinia', async (importOriginal) => {
Object.fromEntries( Object.fromEntries(
Object.entries(store).map(([key, value]) => [ Object.entries(store).map(([key, value]) => [
key, key,
key === 'instanceJoinHistory' ? value : value?.value ?? value key === 'instanceJoinHistory'
? value
: (value?.value ?? value)
]) ])
) )
}; };
@@ -104,7 +116,8 @@ vi.mock('../../coordinators/userCoordinator', () => ({
vi.mock('@/components/ui/button', () => ({ vi.mock('@/components/ui/button', () => ({
Button: { Button: {
emits: ['click'], emits: ['click'],
template: '<button data-testid="btn" @click="$emit(\'click\', $event)"><slot /></button>' template:
'<button data-testid="btn" @click="$emit(\'click\', $event)"><slot /></button>'
} }
})); }));
@@ -157,7 +170,8 @@ function mountBar(props = {}) {
stubs: { stubs: {
TooltipWrapper: { TooltipWrapper: {
props: ['content'], props: ['content'],
template: '<div><slot /><slot name="content" /><span v-if="content">{{ content }}</span></div>' template:
'<div><slot /><slot name="content" /><span v-if="content">{{ content }}</span></div>'
}, },
Timer: { Timer: {
props: ['epoch'], props: ['epoch'],
@@ -185,8 +199,12 @@ describe('InstanceActionBar.vue', () => {
mocks.applyInstance.mockClear(); mocks.applyInstance.mockClear();
mocks.showLaunchDialog.mockClear(); mocks.showLaunchDialog.mockClear();
mocks.tryOpenInstanceInVrc.mockClear(); mocks.tryOpenInstanceInVrc.mockClear();
mocks.modalConfirm.mockImplementation(() => Promise.resolve({ ok: true })); mocks.modalConfirm.mockImplementation(() =>
mocks.instanceJoinHistory.value = new Map([['wrld_base:111', 1700000000]]); Promise.resolve({ ok: true })
);
mocks.instanceJoinHistory.value = new Map([
['wrld_base:111', 1700000000]
]);
mocks.canOpenInstanceInGame = false; mocks.canOpenInstanceInGame = false;
mocks.isOpeningInstance = false; mocks.isOpeningInstance = false;
mocks.lastLocation.location = 'wrld_here:111'; mocks.lastLocation.location = 'wrld_here:111';
@@ -203,8 +221,12 @@ describe('InstanceActionBar.vue', () => {
}); });
expect(wrapper.findAll('[data-testid="btn"]')).toHaveLength(2); expect(wrapper.findAll('[data-testid="btn"]')).toHaveLength(2);
expect(wrapper.text()).toContain('dialog.user.info.launch_invite_tooltip'); expect(wrapper.text()).toContain(
expect(wrapper.text()).toContain('dialog.user.info.self_invite_tooltip'); 'dialog.user.info.launch_invite_tooltip'
);
expect(wrapper.text()).toContain(
'dialog.user.info.self_invite_tooltip'
);
}); });
it('launch button opens launch dialog with resolved launchLocation', async () => { it('launch button opens launch dialog with resolved launchLocation', async () => {
@@ -240,7 +262,9 @@ describe('InstanceActionBar.vue', () => {
worldId: 'wrld_1', worldId: 'wrld_1',
shortName: 'sn' shortName: 'sn'
}); });
expect(mocks.toastSuccess).toHaveBeenCalledWith('message.invite.self_sent'); expect(mocks.toastSuccess).toHaveBeenCalledWith(
'message.invite.self_sent'
);
}); });
it('invite button opens in VRChat when canOpenInstanceInGame is true', async () => { it('invite button opens in VRChat when canOpenInstanceInGame is true', async () => {
@@ -256,7 +280,10 @@ describe('InstanceActionBar.vue', () => {
await inviteBtn.trigger('click'); await inviteBtn.trigger('click');
expect(mocks.tryOpenInstanceInVrc).toHaveBeenCalledWith('wrld_1:inst_1', 'sn'); expect(mocks.tryOpenInstanceInVrc).toHaveBeenCalledWith(
'wrld_1:inst_1',
'sn'
);
expect(mocks.selfInvite).not.toHaveBeenCalled(); expect(mocks.selfInvite).not.toHaveBeenCalled();
}); });
@@ -302,7 +329,11 @@ describe('InstanceActionBar.vue', () => {
} }
}); });
const closeBtn = wrapper.findAll('button').find((btn) => btn.text().includes('dialog.user.info.close_instance')); const closeBtn = wrapper
.findAll('button')
.find((btn) =>
btn.text().includes('dialog.user.info.close_instance')
);
expect(closeBtn).toBeTruthy(); expect(closeBtn).toBeTruthy();
await closeBtn.trigger('click'); await closeBtn.trigger('click');
@@ -311,9 +342,14 @@ describe('InstanceActionBar.vue', () => {
await nextTick(); await nextTick();
expect(mocks.modalConfirm).toHaveBeenCalled(); expect(mocks.modalConfirm).toHaveBeenCalled();
expect(mocks.closeInstance).toHaveBeenCalledWith({ location: 'wrld_close:444', hardClose: false }); expect(mocks.closeInstance).toHaveBeenCalledWith({
location: 'wrld_close:444',
hardClose: false
});
expect(mocks.applyInstance).toHaveBeenCalledWith({ id: 'inst_closed' }); expect(mocks.applyInstance).toHaveBeenCalledWith({ id: 'inst_closed' });
expect(mocks.toastSuccess).toHaveBeenCalledWith('message.instance.closed'); expect(mocks.toastSuccess).toHaveBeenCalledWith(
'message.instance.closed'
);
}); });
it('hides launch and invite buttons when invite-self is not allowed', () => { it('hides launch and invite buttons when invite-self is not allowed', () => {
+17 -4
View File
@@ -7,7 +7,11 @@ const mocks = vi.hoisted(() => ({
showLaunchDialog: vi.fn(), showLaunchDialog: vi.fn(),
showGroupDialog: vi.fn(), showGroupDialog: vi.fn(),
getGroupName: vi.fn(() => Promise.resolve('Fetched Group')), getGroupName: vi.fn(() => Promise.resolve('Fetched Group')),
parseLocation: vi.fn(() => ({ isRealInstance: true, tag: 'wrld_1:inst_1', groupId: 'grp_1' })) parseLocation: vi.fn(() => ({
isRealInstance: true,
tag: 'wrld_1:inst_1',
groupId: 'grp_1'
}))
})); }));
vi.mock('pinia', async (importOriginal) => { vi.mock('pinia', async (importOriginal) => {
@@ -102,13 +106,19 @@ describe('LocationWorld.vue', () => {
mocks.showGroupDialog.mockClear(); mocks.showGroupDialog.mockClear();
mocks.getGroupName.mockClear(); mocks.getGroupName.mockClear();
mocks.parseLocation.mockClear(); mocks.parseLocation.mockClear();
mocks.parseLocation.mockImplementation(() => ({ isRealInstance: true, tag: 'wrld_1:inst_1', groupId: 'grp_1' })); mocks.parseLocation.mockImplementation(() => ({
isRealInstance: true,
tag: 'wrld_1:inst_1',
groupId: 'grp_1'
}));
}); });
it('renders translated access type and instance name', () => { it('renders translated access type and instance name', () => {
const wrapper = mountComponent(); const wrapper = mountComponent();
expect(wrapper.text()).toContain('dialog.world.instance.friends #Instance Name'); expect(wrapper.text()).toContain(
'dialog.world.instance.friends #Instance Name'
);
expect(wrapper.find('.flags.eu').exists()).toBe(true); expect(wrapper.find('.flags.eu').exists()).toBe(true);
}); });
@@ -119,7 +129,10 @@ describe('LocationWorld.vue', () => {
await wrapper.findAll('.cursor-pointer')[0].trigger('click'); await wrapper.findAll('.cursor-pointer')[0].trigger('click');
expect(mocks.showLaunchDialog).toHaveBeenCalledWith('wrld_1:inst_1', 'short-1'); expect(mocks.showLaunchDialog).toHaveBeenCalledWith(
'wrld_1:inst_1',
'short-1'
);
}); });
it('shows group hint and opens group dialog', async () => { it('shows group hint and opens group dialog', async () => {
@@ -695,11 +695,7 @@
const platforms = []; const platforms = [];
if (ref.unityPackages) { if (ref.unityPackages) {
for (const unityPackage of ref.unityPackages) { for (const unityPackage of ref.unityPackages) {
if ( if (unityPackage.variant && unityPackage.variant !== 'standard' && unityPackage.variant !== 'security') {
unityPackage.variant &&
unityPackage.variant !== 'standard' &&
unityPackage.variant !== 'security'
) {
// skip imposters // skip imposters
continue; continue;
} }
+2 -8
View File
@@ -426,12 +426,7 @@
if (byId) return byId; if (byId) return byId;
} }
if ( if (allowIndexFallback && typeof entity.index === 'number' && entity.index >= 0 && entity.index < nodes.length) {
allowIndexFallback &&
typeof entity.index === 'number' &&
entity.index >= 0 &&
entity.index < nodes.length
) {
return nodes[entity.index] || null; return nodes[entity.index] || null;
} }
@@ -620,8 +615,7 @@
const sourceNode = const sourceNode =
(sourceIdSnapshot (sourceIdSnapshot
? visibleNodes.find( ? visibleNodes.find(
(node) => (node) => node.id === sourceIdSnapshot && node.type === (sourceIsFolderSnapshot ? 'folder' : 'item')
node.id === sourceIdSnapshot && node.type === (sourceIsFolderSnapshot ? 'folder' : 'item')
) )
: null) || resolveNodeFromDnDEntity(source, visibleNodes); : null) || resolveNodeFromDnDEntity(source, visibleNodes);
if (!sourceNode) return; if (!sourceNode) return;
@@ -381,8 +381,7 @@
const { showFullscreenImageDialog } = useGalleryStore(); const { showFullscreenImageDialog } = useGalleryStore();
const instanceStore = useInstanceStore(); const instanceStore = useInstanceStore();
const { pastCalenderEvents, upcomingCalenderEvents, updateFollowingCalendarData } = const { pastCalenderEvents, upcomingCalenderEvents, updateFollowingCalendarData } = useGroupCalendarEvents(groupDialog);
useGroupCalendarEvents(groupDialog);
/** /**
* *
@@ -122,11 +122,7 @@
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useAppearanceSettingsStore, useGalleryStore, useGroupStore, useUserStore } from '../../../stores'; import { useAppearanceSettingsStore, useGalleryStore, useGroupStore, useUserStore } from '../../../stores';
import { import { applyGroupMember, handleGroupMember, handleGroupMemberProps } from '../../../coordinators/groupCoordinator';
applyGroupMember,
handleGroupMember,
handleGroupMemberProps
} from '../../../coordinators/groupCoordinator';
import { hasGroupPermission } from '../../../shared/utils'; import { hasGroupPermission } from '../../../shared/utils';
import { useUserDisplay } from '../../../composables/useUserDisplay'; import { useUserDisplay } from '../../../composables/useUserDisplay';
import { groupDialogFilterOptions, groupDialogSortingOptions } from '../../../shared/constants'; import { groupDialogFilterOptions, groupDialogSortingOptions } from '../../../shared/constants';
@@ -74,13 +74,7 @@
{ label: 'description', text: 'dialog.group_member_moderation.description' }, { label: 'description', text: 'dialog.group_member_moderation.description' },
{ label: 'data', text: 'dialog.group_member_moderation.data' } { label: 'data', text: 'dialog.group_member_moderation.data' }
]; ];
const checkedGroupLogsExportLogsOptions = ref([ const checkedGroupLogsExportLogsOptions = ref(['created_at', 'eventType', 'actorDisplayName', 'description', 'data']);
'created_at',
'eventType',
'actorDisplayName',
'description',
'data'
]);
/** /**
* *
@@ -10,7 +10,7 @@ vi.mock('vue-i18n', () => ({
locale: require('vue').ref('en') locale: require('vue').ref('en')
}), }),
createI18n: () => ({ createI18n: () => ({
global: { t: (key) => key , locale: require('vue').ref('en') }, global: { t: (key) => key, locale: require('vue').ref('en') },
install: vi.fn() install: vi.fn()
}) })
})); }));
@@ -3,12 +3,11 @@ import { mount } from '@vue/test-utils';
vi.mock('vue-i18n', () => ({ vi.mock('vue-i18n', () => ({
useI18n: () => ({ useI18n: () => ({
t: (key) => key t: (key) => key,
, locale: require('vue').ref('en')
locale: require('vue').ref('en') }),
}),
createI18n: () => ({ createI18n: () => ({
global: { t: (key) => key , locale: require('vue').ref('en') }, global: { t: (key) => key, locale: require('vue').ref('en') },
install: vi.fn() install: vi.fn()
}) })
})); }));
@@ -53,7 +52,8 @@ function mountComponent(props = {}) {
Trash2: { template: '<svg class="trash-icon" />' }, Trash2: { template: '<svg class="trash-icon" />' },
X: { template: '<svg class="x-icon" />' }, X: { template: '<svg class="x-icon" />' },
TooltipWrapper: { TooltipWrapper: {
template: '<div class="tooltip-stub"><slot /><slot name="content" /></div>' template:
'<div class="tooltip-stub"><slot /><slot name="content" /></div>'
} }
} }
} }
@@ -68,24 +68,32 @@ describe('GroupModerationBulkActions.vue', () => {
describe('rendering', () => { describe('rendering', () => {
test('renders user ID input field', () => { test('renders user ID input field', () => {
const wrapper = mountComponent(); const wrapper = mountComponent();
expect(wrapper.text()).toContain('dialog.group_member_moderation.user_id'); expect(wrapper.text()).toContain(
'dialog.group_member_moderation.user_id'
);
}); });
test('renders selected users section', () => { test('renders selected users section', () => {
const wrapper = mountComponent(); const wrapper = mountComponent();
expect(wrapper.text()).toContain('dialog.group_member_moderation.selected_users'); expect(wrapper.text()).toContain(
'dialog.group_member_moderation.selected_users'
);
}); });
test('renders roles dropdown with available roles', () => { test('renders roles dropdown with available roles', () => {
const wrapper = mountComponent(); const wrapper = mountComponent();
expect(wrapper.text()).toContain('dialog.group_member_moderation.selected_roles'); expect(wrapper.text()).toContain(
'dialog.group_member_moderation.selected_roles'
);
}); });
test('renders action buttons', () => { test('renders action buttons', () => {
const wrapper = mountComponent(); const wrapper = mountComponent();
const text = wrapper.text(); const text = wrapper.text();
expect(text).toContain('dialog.group_member_moderation.add_roles'); expect(text).toContain('dialog.group_member_moderation.add_roles');
expect(text).toContain('dialog.group_member_moderation.remove_roles'); expect(text).toContain(
'dialog.group_member_moderation.remove_roles'
);
expect(text).toContain('dialog.group_member_moderation.save_note'); expect(text).toContain('dialog.group_member_moderation.save_note');
expect(text).toContain('dialog.group_member_moderation.kick'); expect(text).toContain('dialog.group_member_moderation.kick');
expect(text).toContain('dialog.group_member_moderation.ban'); expect(text).toContain('dialog.group_member_moderation.ban');
@@ -95,8 +103,18 @@ describe('GroupModerationBulkActions.vue', () => {
test('renders selected user badges', () => { test('renders selected user badges', () => {
const wrapper = mountComponent({ const wrapper = mountComponent({
selectedUsersArray: [ selectedUsersArray: [
{ id: 'usr_1', userId: 'usr_1', membershipStatus: 'member', user: { displayName: 'Alice' } }, {
{ id: 'usr_2', userId: 'usr_2', membershipStatus: 'member', user: { displayName: 'Bob' } } id: 'usr_1',
userId: 'usr_1',
membershipStatus: 'member',
user: { displayName: 'Alice' }
},
{
id: 'usr_2',
userId: 'usr_2',
membershipStatus: 'member',
user: { displayName: 'Bob' }
}
] ]
}); });
expect(wrapper.text()).toContain('Alice'); expect(wrapper.text()).toContain('Alice');
@@ -106,54 +124,88 @@ describe('GroupModerationBulkActions.vue', () => {
test('shows warning tooltip for non-member users', () => { test('shows warning tooltip for non-member users', () => {
const wrapper = mountComponent({ const wrapper = mountComponent({
selectedUsersArray: [ selectedUsersArray: [
{ id: 'usr_1', userId: 'usr_1', membershipStatus: 'banned', user: { displayName: 'Charlie' } } {
id: 'usr_1',
userId: 'usr_1',
membershipStatus: 'banned',
user: { displayName: 'Charlie' }
}
] ]
}); });
expect(wrapper.text()).toContain('dialog.group_member_moderation.user_isnt_in_group'); expect(wrapper.text()).toContain(
'dialog.group_member_moderation.user_isnt_in_group'
);
}); });
test('does not show warning for member users', () => { test('does not show warning for member users', () => {
const wrapper = mountComponent({ const wrapper = mountComponent({
selectedUsersArray: [ selectedUsersArray: [
{ id: 'usr_1', userId: 'usr_1', membershipStatus: 'member', user: { displayName: 'Alice' } } {
id: 'usr_1',
userId: 'usr_1',
membershipStatus: 'member',
user: { displayName: 'Alice' }
}
] ]
}); });
expect(wrapper.text()).not.toContain('dialog.group_member_moderation.user_isnt_in_group'); expect(wrapper.text()).not.toContain(
'dialog.group_member_moderation.user_isnt_in_group'
);
}); });
}); });
describe('progress indicator', () => { describe('progress indicator', () => {
test('shows progress when progressCurrent > 0', () => { test('shows progress when progressCurrent > 0', () => {
const wrapper = mountComponent({ progressCurrent: 3, progressTotal: 10 }); const wrapper = mountComponent({
expect(wrapper.text()).toContain('dialog.group_member_moderation.progress'); progressCurrent: 3,
progressTotal: 10
});
expect(wrapper.text()).toContain(
'dialog.group_member_moderation.progress'
);
expect(wrapper.text()).toContain('3/10'); expect(wrapper.text()).toContain('3/10');
}); });
test('shows cancel button during progress', () => { test('shows cancel button during progress', () => {
const wrapper = mountComponent({ progressCurrent: 3, progressTotal: 10 }); const wrapper = mountComponent({
expect(wrapper.text()).toContain('dialog.group_member_moderation.cancel'); progressCurrent: 3,
progressTotal: 10
});
expect(wrapper.text()).toContain(
'dialog.group_member_moderation.cancel'
);
}); });
test('hides progress when not in progress', () => { test('hides progress when not in progress', () => {
const wrapper = mountComponent({ progressCurrent: 0 }); const wrapper = mountComponent({ progressCurrent: 0 });
expect(wrapper.text()).not.toContain('dialog.group_member_moderation.progress'); expect(wrapper.text()).not.toContain(
'dialog.group_member_moderation.progress'
);
}); });
}); });
describe('button disabled states', () => { describe('button disabled states', () => {
test('add/remove roles disabled when no roles selected', () => { test('add/remove roles disabled when no roles selected', () => {
const wrapper = mountComponent({ selectedRoles: [] }); const wrapper = mountComponent({ selectedRoles: [] });
const addBtn = wrapper.findAll('button').find((b) => const addBtn = wrapper
b.text().includes('dialog.group_member_moderation.add_roles') .findAll('button')
); .find((b) =>
b
.text()
.includes('dialog.group_member_moderation.add_roles')
);
expect(addBtn.attributes('disabled')).toBeDefined(); expect(addBtn.attributes('disabled')).toBeDefined();
}); });
test('add/remove roles enabled when roles are selected', () => { test('add/remove roles enabled when roles are selected', () => {
const wrapper = mountComponent({ selectedRoles: ['role_1'] }); const wrapper = mountComponent({ selectedRoles: ['role_1'] });
const addBtn = wrapper.findAll('button').find((b) => const addBtn = wrapper
b.text().includes('dialog.group_member_moderation.add_roles') .findAll('button')
); .find((b) =>
b
.text()
.includes('dialog.group_member_moderation.add_roles')
);
expect(addBtn.attributes('disabled')).toBeUndefined(); expect(addBtn.attributes('disabled')).toBeUndefined();
}); });
@@ -163,25 +215,35 @@ describe('GroupModerationBulkActions.vue', () => {
progressCurrent: 5, progressCurrent: 5,
progressTotal: 10 progressTotal: 10
}); });
const kickBtn = wrapper.findAll('button').find((b) => const kickBtn = wrapper
b.text().includes('dialog.group_member_moderation.kick') .findAll('button')
); .find((b) =>
b.text().includes('dialog.group_member_moderation.kick')
);
expect(kickBtn.attributes('disabled')).toBeDefined(); expect(kickBtn.attributes('disabled')).toBeDefined();
}); });
test('select user button disabled when no user ID entered', () => { test('select user button disabled when no user ID entered', () => {
const wrapper = mountComponent({ selectUserId: '' }); const wrapper = mountComponent({ selectUserId: '' });
const selectBtn = wrapper.findAll('button').find((b) => const selectBtn = wrapper
b.text().includes('dialog.group_member_moderation.select_user') .findAll('button')
); .find((b) =>
b
.text()
.includes('dialog.group_member_moderation.select_user')
);
expect(selectBtn.attributes('disabled')).toBeDefined(); expect(selectBtn.attributes('disabled')).toBeDefined();
}); });
test('select user button enabled when user ID is entered', () => { test('select user button enabled when user ID is entered', () => {
const wrapper = mountComponent({ selectUserId: 'usr_test' }); const wrapper = mountComponent({ selectUserId: 'usr_test' });
const selectBtn = wrapper.findAll('button').find((b) => const selectBtn = wrapper
b.text().includes('dialog.group_member_moderation.select_user') .findAll('button')
); .find((b) =>
b
.text()
.includes('dialog.group_member_moderation.select_user')
);
expect(selectBtn.attributes('disabled')).toBeUndefined(); expect(selectBtn.attributes('disabled')).toBeUndefined();
}); });
}); });
@@ -194,9 +256,11 @@ describe('GroupModerationBulkActions.vue', () => {
_mockPermissions: ['group-bans-manage'] _mockPermissions: ['group-bans-manage']
} }
}); });
const kickBtn = wrapper.findAll('button').find((b) => const kickBtn = wrapper
b.text().includes('dialog.group_member_moderation.kick') .findAll('button')
); .find((b) =>
b.text().includes('dialog.group_member_moderation.kick')
);
expect(kickBtn.attributes('disabled')).toBeDefined(); expect(kickBtn.attributes('disabled')).toBeDefined();
}); });
@@ -207,12 +271,16 @@ describe('GroupModerationBulkActions.vue', () => {
_mockPermissions: ['group-members-remove'] _mockPermissions: ['group-members-remove']
} }
}); });
const banBtn = wrapper.findAll('button').find((b) => const banBtn = wrapper
b.text().includes('dialog.group_member_moderation.ban') .findAll('button')
); .find((b) =>
const unbanBtn = wrapper.findAll('button').find((b) => b.text().includes('dialog.group_member_moderation.ban')
b.text().includes('dialog.group_member_moderation.unban') );
); const unbanBtn = wrapper
.findAll('button')
.find((b) =>
b.text().includes('dialog.group_member_moderation.unban')
);
expect(banBtn.attributes('disabled')).toBeDefined(); expect(banBtn.attributes('disabled')).toBeDefined();
expect(unbanBtn.attributes('disabled')).toBeDefined(); expect(unbanBtn.attributes('disabled')).toBeDefined();
}); });
@@ -221,9 +289,13 @@ describe('GroupModerationBulkActions.vue', () => {
describe('events', () => { describe('events', () => {
test('emits select-user on select button click', async () => { test('emits select-user on select button click', async () => {
const wrapper = mountComponent({ selectUserId: 'usr_test' }); const wrapper = mountComponent({ selectUserId: 'usr_test' });
const selectBtn = wrapper.findAll('button').find((b) => const selectBtn = wrapper
b.text().includes('dialog.group_member_moderation.select_user') .findAll('button')
); .find((b) =>
b
.text()
.includes('dialog.group_member_moderation.select_user')
);
await selectBtn.trigger('click'); await selectBtn.trigger('click');
expect(wrapper.emitted('select-user')).toBeTruthy(); expect(wrapper.emitted('select-user')).toBeTruthy();
}); });
@@ -241,7 +313,12 @@ describe('GroupModerationBulkActions.vue', () => {
}); });
test('emits delete-user when removing a selected user', async () => { test('emits delete-user when removing a selected user', async () => {
const user = { id: 'usr_1', userId: 'usr_1', membershipStatus: 'member', user: { displayName: 'Alice' } }; const user = {
id: 'usr_1',
userId: 'usr_1',
membershipStatus: 'member',
user: { displayName: 'Alice' }
};
const wrapper = mountComponent({ selectedUsersArray: [user] }); const wrapper = mountComponent({ selectedUsersArray: [user] });
// The X button is a native <button type="button"> inside each Badge // The X button is a native <button type="button"> inside each Badge
const deleteBtn = wrapper.find('button[type="button"]'); const deleteBtn = wrapper.find('button[type="button"]');
@@ -251,18 +328,25 @@ describe('GroupModerationBulkActions.vue', () => {
test('emits ban on ban button click', async () => { test('emits ban on ban button click', async () => {
const wrapper = mountComponent(); const wrapper = mountComponent();
const banBtn = wrapper.findAll('button').find((b) => const banBtn = wrapper
b.text().includes('dialog.group_member_moderation.ban') .findAll('button')
); .find((b) =>
b.text().includes('dialog.group_member_moderation.ban')
);
await banBtn.trigger('click'); await banBtn.trigger('click');
expect(wrapper.emitted('ban')).toBeTruthy(); expect(wrapper.emitted('ban')).toBeTruthy();
}); });
test('emits cancel-progress on cancel click', async () => { test('emits cancel-progress on cancel click', async () => {
const wrapper = mountComponent({ progressCurrent: 3, progressTotal: 10 }); const wrapper = mountComponent({
const cancelBtn = wrapper.findAll('button').find((b) => progressCurrent: 3,
b.text().includes('dialog.group_member_moderation.cancel') progressTotal: 10
); });
const cancelBtn = wrapper
.findAll('button')
.find((b) =>
b.text().includes('dialog.group_member_moderation.cancel')
);
await cancelBtn.trigger('click'); await cancelBtn.trigger('click');
expect(wrapper.emitted('cancel-progress')).toBeTruthy(); expect(wrapper.emitted('cancel-progress')).toBeTruthy();
}); });
@@ -54,7 +54,10 @@ describe('useGroupDialogCommands', () => {
it('returns early when dialog is not visible', () => { it('returns early when dialog is not visible', () => {
const groupDialog = createGroupDialog({ visible: false }); const groupDialog = createGroupDialog({ visible: false });
const deps = createDeps(); const deps = createDeps();
const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps); const { groupDialogCommand } = useGroupDialogCommands(
groupDialog,
deps
);
groupDialogCommand('Refresh'); groupDialogCommand('Refresh');
expect(deps.showGroupDialog).not.toHaveBeenCalled(); expect(deps.showGroupDialog).not.toHaveBeenCalled();
@@ -63,7 +66,10 @@ describe('useGroupDialogCommands', () => {
it('Share copies group URL', () => { it('Share copies group URL', () => {
const groupDialog = createGroupDialog(); const groupDialog = createGroupDialog();
const deps = createDeps(); const deps = createDeps();
const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps); const { groupDialogCommand } = useGroupDialogCommands(
groupDialog,
deps
);
groupDialogCommand('Share'); groupDialogCommand('Share');
expect(copyToClipboard).toHaveBeenCalledWith( expect(copyToClipboard).toHaveBeenCalledWith(
@@ -74,7 +80,10 @@ describe('useGroupDialogCommands', () => {
it('Invite To Group dispatches invite callback', () => { it('Invite To Group dispatches invite callback', () => {
const groupDialog = createGroupDialog(); const groupDialog = createGroupDialog();
const deps = createDeps(); const deps = createDeps();
const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps); const { groupDialogCommand } = useGroupDialogCommands(
groupDialog,
deps
);
groupDialogCommand('Invite To Group'); groupDialogCommand('Invite To Group');
expect(deps.showInviteGroupDialog).toHaveBeenCalledWith('grp_123', ''); expect(deps.showInviteGroupDialog).toHaveBeenCalledWith('grp_123', '');
@@ -83,7 +92,10 @@ describe('useGroupDialogCommands', () => {
it('Refresh calls showGroupDialog with forceRefresh', () => { it('Refresh calls showGroupDialog with forceRefresh', () => {
const groupDialog = createGroupDialog(); const groupDialog = createGroupDialog();
const deps = createDeps(); const deps = createDeps();
const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps); const { groupDialogCommand } = useGroupDialogCommands(
groupDialog,
deps
);
groupDialogCommand('Refresh'); groupDialogCommand('Refresh');
expect(deps.showGroupDialog).toHaveBeenCalledWith('grp_123', { expect(deps.showGroupDialog).toHaveBeenCalledWith('grp_123', {
@@ -94,7 +106,10 @@ describe('useGroupDialogCommands', () => {
it('Block Group confirms and calls blockGroup', async () => { it('Block Group confirms and calls blockGroup', async () => {
const groupDialog = createGroupDialog(); const groupDialog = createGroupDialog();
const deps = createDeps(); const deps = createDeps();
const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps); const { groupDialogCommand } = useGroupDialogCommands(
groupDialog,
deps
);
groupDialogCommand('Block Group'); groupDialogCommand('Block Group');
await vi.waitFor(() => { await vi.waitFor(() => {
@@ -109,7 +124,10 @@ describe('useGroupDialogCommands', () => {
it('Unblock Group confirms and calls unblockGroup', async () => { it('Unblock Group confirms and calls unblockGroup', async () => {
const groupDialog = createGroupDialog(); const groupDialog = createGroupDialog();
const deps = createDeps(); const deps = createDeps();
const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps); const { groupDialogCommand } = useGroupDialogCommands(
groupDialog,
deps
);
groupDialogCommand('Unblock Group'); groupDialogCommand('Unblock Group');
await vi.waitFor(() => { await vi.waitFor(() => {
@@ -129,7 +147,10 @@ describe('useGroupDialogCommands', () => {
confirm: vi.fn().mockResolvedValue({ ok: false }) confirm: vi.fn().mockResolvedValue({ ok: false })
} }
}); });
const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps); const { groupDialogCommand } = useGroupDialogCommands(
groupDialog,
deps
);
groupDialogCommand('Block Group'); groupDialogCommand('Block Group');
await vi.waitFor(() => { await vi.waitFor(() => {
@@ -233,9 +233,7 @@ describe('useGroupGalleries', () => {
.spyOn(console, 'error') .spyOn(console, 'error')
.mockImplementation(() => {}); .mockImplementation(() => {});
queryRequest.fetch.mockRejectedValueOnce( queryRequest.fetch.mockRejectedValueOnce(new Error('API Error'));
new Error('API Error')
);
await expect( await expect(
getGroupGallery('grp_1', 'g1') getGroupGallery('grp_1', 'g1')
@@ -4,12 +4,11 @@ import { describe, expect, test, vi, beforeEach } from 'vitest';
vi.mock('vue-sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } })); vi.mock('vue-sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
vi.mock('vue-i18n', () => ({ vi.mock('vue-i18n', () => ({
useI18n: () => ({ useI18n: () => ({
t: (key) => key t: (key) => key,
, locale: require('vue').ref('en')
locale: require('vue').ref('en') }),
}),
createI18n: () => ({ createI18n: () => ({
global: { t: (key) => key , locale: require('vue').ref('en') }, global: { t: (key) => key, locale: require('vue').ref('en') },
install: vi.fn() install: vi.fn()
}) })
})); }));
@@ -95,11 +94,19 @@ import { queryRequest } from '../../../../api';
function createTables() { function createTables() {
return { return {
members: reactive({ data: [], pageSize: 15 }), members: reactive({ data: [], pageSize: 15 }),
bans: reactive({ data: [], filters: [{ prop: ['$displayName'], value: '' }], pageSize: 15 }), bans: reactive({
data: [],
filters: [{ prop: ['$displayName'], value: '' }],
pageSize: 15
}),
invites: reactive({ data: [], pageSize: 15 }), invites: reactive({ data: [], pageSize: 15 }),
joinRequests: reactive({ data: [], pageSize: 15 }), joinRequests: reactive({ data: [], pageSize: 15 }),
blocked: reactive({ data: [], pageSize: 15 }), blocked: reactive({ data: [], pageSize: 15 }),
logs: reactive({ data: [], filters: [{ prop: ['description'], value: '' }], pageSize: 15 }) logs: reactive({
data: [],
filters: [{ prop: ['description'], value: '' }],
pageSize: 15
})
}; };
} }
@@ -167,11 +174,19 @@ describe('useGroupModerationData', () => {
userId: `usr_${i}`, userId: `usr_${i}`,
user: { displayName: `User${i}` } user: { displayName: `User${i}` }
})); }));
const page2 = [{ userId: 'usr_100', user: { displayName: 'User100' } }]; const page2 = [
{ userId: 'usr_100', user: { displayName: 'User100' } }
];
deps.groupRequest.getGroupBans deps.groupRequest.getGroupBans
.mockResolvedValueOnce({ json: page1, params: { groupId: 'grp_test' } }) .mockResolvedValueOnce({
.mockResolvedValueOnce({ json: page2, params: { groupId: 'grp_test' } }); json: page1,
params: { groupId: 'grp_test' }
})
.mockResolvedValueOnce({
json: page2,
params: { groupId: 'grp_test' }
});
const { getAllGroupBans } = useGroupModerationData(deps); const { getAllGroupBans } = useGroupModerationData(deps);
await getAllGroupBans('grp_test'); await getAllGroupBans('grp_test');
@@ -198,12 +213,17 @@ describe('useGroupModerationData', () => {
test('handles API error gracefully', async () => { test('handles API error gracefully', async () => {
const { toast } = await import('vue-sonner'); const { toast } = await import('vue-sonner');
const deps = createDeps(); const deps = createDeps();
deps.groupRequest.getGroupBans.mockRejectedValue(new Error('Network error')); deps.groupRequest.getGroupBans.mockRejectedValue(
new Error('Network error')
);
const { getAllGroupBans, isGroupMembersLoading } = useGroupModerationData(deps); const { getAllGroupBans, isGroupMembersLoading } =
useGroupModerationData(deps);
await getAllGroupBans('grp_test'); await getAllGroupBans('grp_test');
expect(toast.error).toHaveBeenCalledWith('Failed to get group bans'); expect(toast.error).toHaveBeenCalledWith(
'Failed to get group bans'
);
expect(isGroupMembersLoading.value).toBe(false); expect(isGroupMembersLoading.value).toBe(false);
}); });
@@ -213,10 +233,16 @@ describe('useGroupModerationData', () => {
userId: `usr_${i}` userId: `usr_${i}`
})); }));
deps.groupRequest.getGroupBans deps.groupRequest.getGroupBans
.mockResolvedValueOnce({ json: page1, params: { groupId: 'grp_test' } }) .mockResolvedValueOnce({
json: page1,
params: { groupId: 'grp_test' }
})
.mockImplementation(() => { .mockImplementation(() => {
deps.groupMemberModeration.value.visible = false; deps.groupMemberModeration.value.visible = false;
return Promise.resolve({ json: [{ userId: 'usr_extra' }], params: { groupId: 'grp_test' } }); return Promise.resolve({
json: [{ userId: 'usr_extra' }],
params: { groupId: 'grp_test' }
});
}); });
const { getAllGroupBans } = useGroupModerationData(deps); const { getAllGroupBans } = useGroupModerationData(deps);
@@ -254,7 +280,10 @@ describe('useGroupModerationData', () => {
}); });
const { getAllGroupLogs } = useGroupModerationData(deps); const { getAllGroupLogs } = useGroupModerationData(deps);
await getAllGroupLogs('grp_test', ['group.member.ban', 'group.member.kick']); await getAllGroupLogs('grp_test', [
'group.member.ban',
'group.member.kick'
]);
expect(deps.groupRequest.getGroupLogs).toHaveBeenCalledWith( expect(deps.groupRequest.getGroupLogs).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@@ -281,7 +310,8 @@ describe('useGroupModerationData', () => {
params: { groupId: 'grp_test' } params: { groupId: 'grp_test' }
}); });
const { getAllGroupInvitesAndJoinRequests } = useGroupModerationData(deps); const { getAllGroupInvitesAndJoinRequests } =
useGroupModerationData(deps);
await getAllGroupInvitesAndJoinRequests('grp_test'); await getAllGroupInvitesAndJoinRequests('grp_test');
expect(deps.tables.invites.data).toHaveLength(1); expect(deps.tables.invites.data).toHaveLength(1);
@@ -294,7 +324,10 @@ describe('useGroupModerationData', () => {
test('parses multiple user IDs from input', async () => { test('parses multiple user IDs from input', async () => {
const deps = createDeps(); const deps = createDeps();
deps.groupRequest.getGroupMember.mockResolvedValue({ deps.groupRequest.getGroupMember.mockResolvedValue({
json: { userId: 'usr_aaaa1111-2222-3333-4444-555566667777', user: { displayName: 'A' } }, json: {
userId: 'usr_aaaa1111-2222-3333-4444-555566667777',
user: { displayName: 'A' }
},
params: {} params: {}
}); });
@@ -344,7 +377,10 @@ describe('useGroupModerationData', () => {
const { addGroupMemberToSelection } = useGroupModerationData(deps); const { addGroupMemberToSelection } = useGroupModerationData(deps);
await addGroupMemberToSelection('usr_1'); await addGroupMemberToSelection('usr_1');
expect(deps.selection.setSelectedUsers).toHaveBeenCalledWith('usr_1', member); expect(deps.selection.setSelectedUsers).toHaveBeenCalledWith(
'usr_1',
member
);
expect(queryRequest.fetch).not.toHaveBeenCalled(); expect(queryRequest.fetch).not.toHaveBeenCalled();
}); });
@@ -362,11 +398,16 @@ describe('useGroupModerationData', () => {
const { addGroupMemberToSelection } = useGroupModerationData(deps); const { addGroupMemberToSelection } = useGroupModerationData(deps);
await addGroupMemberToSelection('usr_1'); await addGroupMemberToSelection('usr_1');
expect(queryRequest.fetch).toHaveBeenCalledWith('user.dialog', { userId: 'usr_1' }); expect(queryRequest.fetch).toHaveBeenCalledWith('user.dialog', {
expect(deps.selection.setSelectedUsers).toHaveBeenCalledWith('usr_1', expect.objectContaining({ userId: 'usr_1'
userId: 'usr_1', });
displayName: 'Alice' expect(deps.selection.setSelectedUsers).toHaveBeenCalledWith(
})); 'usr_1',
expect.objectContaining({
userId: 'usr_1',
displayName: 'Alice'
})
);
}); });
}); });
@@ -395,7 +436,8 @@ describe('useGroupModerationData', () => {
const deps = createDeps(); const deps = createDeps();
deps.tables.members.data = [{ userId: 'usr_1' }]; deps.tables.members.data = [{ userId: 'usr_1' }];
const { groupMembersSearch, memberSearch, isGroupMembersLoading } = useGroupModerationData(deps); const { groupMembersSearch, memberSearch, isGroupMembersLoading } =
useGroupModerationData(deps);
memberSearch.value = 'ab'; memberSearch.value = 'ab';
groupMembersSearch(); groupMembersSearch();
@@ -405,9 +447,13 @@ describe('useGroupModerationData', () => {
test('setGroupMemberSortOrder does nothing when sort is the same', async () => { test('setGroupMemberSortOrder does nothing when sort is the same', async () => {
const deps = createDeps(); const deps = createDeps();
deps.groupRequest.getGroupMember.mockResolvedValue({ json: null, params: {} }); deps.groupRequest.getGroupMember.mockResolvedValue({
json: null,
params: {}
});
const { setGroupMemberSortOrder, memberSortOrder } = useGroupModerationData(deps); const { setGroupMemberSortOrder, memberSortOrder } =
useGroupModerationData(deps);
const currentSort = memberSortOrder.value; const currentSort = memberSortOrder.value;
await setGroupMemberSortOrder(currentSort); await setGroupMemberSortOrder(currentSort);
@@ -418,7 +464,8 @@ describe('useGroupModerationData', () => {
test('setGroupMemberFilter does nothing when filter is the same', async () => { test('setGroupMemberFilter does nothing when filter is the same', async () => {
const deps = createDeps(); const deps = createDeps();
const { setGroupMemberFilter, memberFilter } = useGroupModerationData(deps); const { setGroupMemberFilter, memberFilter } =
useGroupModerationData(deps);
const currentFilter = memberFilter.value; const currentFilter = memberFilter.value;
await setGroupMemberFilter(currentFilter); await setGroupMemberFilter(currentFilter);
@@ -429,7 +476,8 @@ describe('useGroupModerationData', () => {
describe('loadAllGroupMembers', () => { describe('loadAllGroupMembers', () => {
test('does nothing when already loading', async () => { test('does nothing when already loading', async () => {
const deps = createDeps(); const deps = createDeps();
const { loadAllGroupMembers, isGroupMembersLoading } = useGroupModerationData(deps); const { loadAllGroupMembers, isGroupMembersLoading } =
useGroupModerationData(deps);
isGroupMembersLoading.value = true; isGroupMembersLoading.value = true;
await loadAllGroupMembers(); await loadAllGroupMembers();
@@ -174,11 +174,7 @@
}); });
}; };
addFriendGroup( addFriendGroup('friendsInInstance', t('dialog.invite.friends_in_instance'), props.inviteDialog?.friendsInInstance);
'friendsInInstance',
t('dialog.invite.friends_in_instance'),
props.inviteDialog?.friendsInInstance
);
addFriendGroup('vip', t('side_panel.favorite'), vipFriends.value); addFriendGroup('vip', t('side_panel.favorite'), vipFriends.value);
addFriendGroup('online', t('side_panel.online'), onlineFriends.value); addFriendGroup('online', t('side_panel.online'), onlineFriends.value);
addFriendGroup('active', t('side_panel.active'), activeFriends.value); addFriendGroup('active', t('side_panel.active'), activeFriends.value);
@@ -47,13 +47,7 @@
useUserStore, useUserStore,
useVrcxStore useVrcxStore
} from '../../../stores'; } from '../../../stores';
import { import { compareByCreatedAt, localeIncludes, parseLocation, removeFromArray, timeToText } from '../../../shared/utils';
compareByCreatedAt,
localeIncludes,
parseLocation,
removeFromArray,
timeToText
} from '../../../shared/utils';
import { DataTableLayout } from '../../ui/data-table'; import { DataTableLayout } from '../../ui/data-table';
import { createPreviousInstancesColumns } from './previousInstancesColumns.jsx'; import { createPreviousInstancesColumns } from './previousInstancesColumns.jsx';
import { database } from '../../../services/database'; import { database } from '../../../services/database';
@@ -176,9 +170,7 @@
function deleteGameLogInstancePrompt(row) { function deleteGameLogInstancePrompt(row) {
const description = const description =
props.variant === 'user' props.variant === 'user' ? 'Continue? Delete User From GameLog Instance' : 'Continue? Delete GameLog Instance';
? 'Continue? Delete User From GameLog Instance'
: 'Continue? Delete GameLog Instance';
modalStore modalStore
.confirm({ .confirm({
description, description,
@@ -497,7 +497,7 @@
useInstanceStore, useInstanceStore,
useLocationStore, useLocationStore,
useModalStore, useModalStore,
useUserStore, useUserStore
} from '../../../stores'; } from '../../../stores';
import { showWorldDialog } from '../../../coordinators/worldCoordinator'; import { showWorldDialog } from '../../../coordinators/worldCoordinator';
import { queryRequest, userRequest } from '../../../api'; import { queryRequest, userRequest } from '../../../api';
@@ -506,7 +506,6 @@
import { showUserDialog } from '../../../coordinators/userCoordinator'; import { showUserDialog } from '../../../coordinators/userCoordinator';
import { showGroupDialog } from '../../../coordinators/groupCoordinator'; import { showGroupDialog } from '../../../coordinators/groupCoordinator';
const EditNoteAndMemoDialog = defineAsyncComponent(() => import('./EditNoteAndMemoDialog.vue')); const EditNoteAndMemoDialog = defineAsyncComponent(() => import('./EditNoteAndMemoDialog.vue'));
defineEmits(['showBioDialog']); defineEmits(['showBioDialog']);
@@ -61,12 +61,7 @@
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { import { compareByDisplayName, compareByFriendOrder, compareByLastActiveRef, userImage } from '../../../shared/utils';
compareByDisplayName,
compareByFriendOrder,
compareByLastActiveRef,
userImage
} from '../../../shared/utils';
import { database } from '../../../services/database'; import { database } from '../../../services/database';
import { processBulk } from '../../../services/request'; import { processBulk } from '../../../services/request';
import { useOptionKeySelect } from '../../../composables/useOptionKeySelect'; import { useOptionKeySelect } from '../../../composables/useOptionKeySelect';
@@ -249,13 +249,7 @@
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { import { formatDateFilter, languageClass, openDiscordProfile, userImage, userStatusClass } from '../../../shared/utils';
formatDateFilter,
languageClass,
openDiscordProfile,
userImage,
userStatusClass
} from '../../../shared/utils';
import { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover';
import { useGalleryStore, useUserStore } from '../../../stores'; import { useGalleryStore, useUserStore } from '../../../stores';
import { Badge } from '../../ui/badge'; import { Badge } from '../../ui/badge';
@@ -2,20 +2,61 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { ref } from 'vue'; import { ref } from 'vue';
const mocks = vi.hoisted(() => ({ saveUserMemo: vi.fn(), saveNote: vi.fn(async () => ({ json: { note: 'n1' }, params: { targetUserId: 'usr_1', note: 'n1' } })), getUser: vi.fn() })); const mocks = vi.hoisted(() => ({
saveUserMemo: vi.fn(),
saveNote: vi.fn(async () => ({
json: { note: 'n1' },
params: { targetUserId: 'usr_1', note: 'n1' }
})),
getUser: vi.fn()
}));
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s })); vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) })); vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('../../../../stores', () => ({ vi.mock('../../../../stores', () => ({
useUserStore: () => ({ userDialog: ref({ id: 'usr_1', note: 'n1', memo: 'm1', ref: { id: 'usr_1', note: 'n1' } }), cachedUsers: new Map([['usr_1', { note: 'n1' }]]) }), useUserStore: () => ({
useAppearanceSettingsStore: () => ({ hideUserNotes: ref(false), hideUserMemos: ref(false) }) userDialog: ref({
id: 'usr_1',
note: 'n1',
memo: 'm1',
ref: { id: 'usr_1', note: 'n1' }
}),
cachedUsers: new Map([['usr_1', { note: 'n1' }]])
}),
useAppearanceSettingsStore: () => ({
hideUserNotes: ref(false),
hideUserMemos: ref(false)
})
}));
vi.mock('../../../../api', () => ({
miscRequest: { saveNote: (...a) => mocks.saveNote(...a) },
userRequest: { getUser: (...a) => mocks.getUser(...a) }
}));
vi.mock('../../../../coordinators/memoCoordinator', () => ({
saveUserMemo: (...a) => mocks.saveUserMemo(...a)
})); }));
vi.mock('../../../../api', () => ({ miscRequest: { saveNote: (...a) => mocks.saveNote(...a) }, userRequest: { getUser: (...a) => mocks.getUser(...a) } }));
vi.mock('../../../../coordinators/memoCoordinator', () => ({ saveUserMemo: (...a) => mocks.saveUserMemo(...a) }));
vi.mock('../../../../shared/utils', () => ({ replaceBioSymbols: (s) => s })); vi.mock('../../../../shared/utils', () => ({ replaceBioSymbols: (s) => s }));
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' }, DialogHeader: { template: '<div><slot /></div>' }, DialogTitle: { template: '<div><slot /></div>' }, DialogFooter: { template: '<div><slot /></div>' } })); vi.mock('@/components/ui/dialog', () => ({
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>' } })); Dialog: { template: '<div><slot /></div>' },
vi.mock('@/components/ui/input-group', () => ({ InputGroupTextareaField: { props: ['modelValue'], emits: ['update:modelValue'], template: '<textarea />' } })); DialogContent: { template: '<div><slot /></div>' },
DialogHeader: { template: '<div><slot /></div>' },
DialogTitle: { template: '<div><slot /></div>' },
DialogFooter: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('@/components/ui/input-group', () => ({
InputGroupTextareaField: {
props: ['modelValue'],
emits: ['update:modelValue'],
template: '<textarea />'
}
}));
import EditNoteAndMemoDialog from '../EditNoteAndMemoDialog.vue'; import EditNoteAndMemoDialog from '../EditNoteAndMemoDialog.vue';
@@ -25,7 +66,9 @@ describe('EditNoteAndMemoDialog.vue', () => {
}); });
it('emits close and saves memo on confirm', async () => { it('emits close and saves memo on confirm', async () => {
const wrapper = mount(EditNoteAndMemoDialog, { props: { visible: false } }); const wrapper = mount(EditNoteAndMemoDialog, {
props: { visible: false }
});
await wrapper.setProps({ visible: true }); await wrapper.setProps({ visible: true });
const buttons = wrapper.findAll('[data-testid="btn"]'); const buttons = wrapper.findAll('[data-testid="btn"]');
await buttons[1].trigger('click'); await buttons[1].trigger('click');
@@ -5,14 +5,49 @@ import { ref } from 'vue';
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s })); vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) })); vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('../../../../stores', () => ({ vi.mock('../../../../stores', () => ({
useUserStore: () => ({ userDialog: ref({ ref: { id: 'usr_2', $isModerator: false }, isFriend: false, isFavorite: false, incomingRequest: false, outgoingRequest: false, isBlock: false, isMute: false, isMuteChat: false, isInteractOff: false, isHideAvatar: false, isShowAvatar: false }), currentUser: ref({ id: 'usr_1', isBoopingEnabled: true }) }), useUserStore: () => ({
userDialog: ref({
ref: { id: 'usr_2', $isModerator: false },
isFriend: false,
isFavorite: false,
incomingRequest: false,
outgoingRequest: false,
isBlock: false,
isMute: false,
isMuteChat: false,
isInteractOff: false,
isHideAvatar: false,
isShowAvatar: false
}),
currentUser: ref({ id: 'usr_1', isBoopingEnabled: true })
}),
useGameStore: () => ({ isGameRunning: ref(false) }), useGameStore: () => ({ isGameRunning: ref(false) }),
useLocationStore: () => ({ lastLocation: ref({ location: 'wrld_1:1' }) }) useLocationStore: () => ({ lastLocation: ref({ location: 'wrld_1:1' }) })
})); }));
vi.mock('../../../../composables/useInviteChecks', () => ({ useInviteChecks: () => ({ checkCanInvite: () => true }) })); vi.mock('../../../../composables/useInviteChecks', () => ({
vi.mock('../../../ui/dropdown-menu', () => ({ DropdownMenu: { template: '<div><slot /></div>' }, DropdownMenuTrigger: { template: '<div><slot /></div>' }, DropdownMenuContent: { template: '<div><slot /></div>' }, DropdownMenuSeparator: { template: '<hr />' }, DropdownMenuItem: { emits: ['click'], template: '<button data-testid="dd-item" @click="$emit(\'click\')"><slot /></button>' } })); useInviteChecks: () => ({ checkCanInvite: () => true })
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>' } })); }));
vi.mock('../../../ui/tooltip', () => ({ TooltipWrapper: { template: '<div><slot /></div>' } })); vi.mock('../../../ui/dropdown-menu', () => ({
DropdownMenu: { template: '<div><slot /></div>' },
DropdownMenuTrigger: { template: '<div><slot /></div>' },
DropdownMenuContent: { template: '<div><slot /></div>' },
DropdownMenuSeparator: { template: '<hr />' },
DropdownMenuItem: {
emits: ['click'],
template:
'<button data-testid="dd-item" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('../../../ui/tooltip', () => ({
TooltipWrapper: { template: '<div><slot /></div>' }
}));
vi.mock('lucide-vue-next', () => ({ vi.mock('lucide-vue-next', () => ({
Check: { template: '<i />' }, Check: { template: '<i />' },
CheckCircle: { template: '<i />' }, CheckCircle: { template: '<i />' },
@@ -42,7 +77,9 @@ import UserActionDropdown from '../UserActionDropdown.vue';
describe('UserActionDropdown.vue', () => { describe('UserActionDropdown.vue', () => {
it('forwards command callback from dropdown item', async () => { it('forwards command callback from dropdown item', async () => {
const userDialogCommand = vi.fn(); const userDialogCommand = vi.fn();
const wrapper = mount(UserActionDropdown, { props: { userDialogCommand } }); const wrapper = mount(UserActionDropdown, {
props: { userDialogCommand }
});
await wrapper.findAll('[data-testid="dd-item"]')[0].trigger('click'); await wrapper.findAll('[data-testid="dd-item"]')[0].trigger('click');
@@ -6,7 +6,8 @@ vi.mock('vue-i18n', () => ({
useI18n: () => { useI18n: () => {
const { ref } = require('vue'); const { ref } = require('vue');
return { return {
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key), t: (key, params) =>
params ? `${key}:${JSON.stringify(params)}` : key,
locale: ref('en') locale: ref('en')
}; };
}, },
@@ -242,7 +243,9 @@ describe('UserDialogInfoTab.vue', () => {
test('renders imported InstanceActionBar and Spinner components when conditions are met', () => { test('renders imported InstanceActionBar and Spinner components when conditions are met', () => {
const wrapper = mountComponent(); const wrapper = mountComponent();
expect(wrapper.find('instance-action-bar-stub').exists()).toBe(true); expect(wrapper.find('instance-action-bar-stub').exists()).toBe(
true
);
expect(wrapper.find('spinner-stub').exists()).toBe(true); expect(wrapper.find('spinner-stub').exists()).toBe(true);
}); });
}); });
@@ -10,7 +10,7 @@ vi.mock('vue-i18n', () => ({
locale: require('vue').ref('en') locale: require('vue').ref('en')
}), }),
createI18n: () => ({ createI18n: () => ({
global: { t: (key) => key , locale: require('vue').ref('en') }, global: { t: (key) => key, locale: require('vue').ref('en') },
install: vi.fn() install: vi.fn()
}) })
})); }));
@@ -6,15 +6,34 @@ const mocks = vi.hoisted(() => ({
addFavorite: vi.fn(() => Promise.resolve()), addFavorite: vi.fn(() => Promise.resolve()),
deleteFavoriteNoConfirm: vi.fn(), deleteFavoriteNoConfirm: vi.fn(),
toastSuccess: vi.fn(), toastSuccess: vi.fn(),
favoriteDialog: { __v_isRef: true, value: { visible: true, type: 'friend', objectId: 'usr_1', currentGroup: null } } favoriteDialog: {
__v_isRef: true,
value: {
visible: true,
type: 'friend',
objectId: 'usr_1',
currentGroup: null
}
}
})); }));
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s })); vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) })); vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('vue-sonner', () => ({ toast: { success: (...a) => mocks.toastSuccess(...a) } })); vi.mock('vue-sonner', () => ({
toast: { success: (...a) => mocks.toastSuccess(...a) }
}));
vi.mock('../../../stores', () => ({ vi.mock('../../../stores', () => ({
useFavoriteStore: () => ({ useFavoriteStore: () => ({
favoriteFriendGroups: ref([{ key: 'group_1', type: 'friend', name: 'group_1', displayName: 'G1', count: 0, capacity: 100 }]), favoriteFriendGroups: ref([
{
key: 'group_1',
type: 'friend',
name: 'group_1',
displayName: 'G1',
count: 0,
capacity: 100
}
]),
favoriteAvatarGroups: ref([]), favoriteAvatarGroups: ref([]),
favoriteWorldGroups: ref([]), favoriteWorldGroups: ref([]),
favoriteDialog: mocks.favoriteDialog, favoriteDialog: mocks.favoriteDialog,
@@ -31,9 +50,22 @@ vi.mock('../../../stores', () => ({
}), }),
useUserStore: () => ({ isLocalUserVrcPlusSupporter: ref(true) }) useUserStore: () => ({ isLocalUserVrcPlusSupporter: ref(true) })
})); }));
vi.mock('../../../api', () => ({ favoriteRequest: { addFavorite: (...a) => mocks.addFavorite(...a) } })); vi.mock('../../../api', () => ({
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' }, DialogHeader: { template: '<div><slot /></div>' }, DialogTitle: { template: '<div><slot /></div>' } })); favoriteRequest: { addFavorite: (...a) => mocks.addFavorite(...a) }
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>' } })); }));
vi.mock('@/components/ui/dialog', () => ({
Dialog: { template: '<div><slot /></div>' },
DialogContent: { template: '<div><slot /></div>' },
DialogHeader: { template: '<div><slot /></div>' },
DialogTitle: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('lucide-vue-next', () => ({ Check: { template: '<i />' } })); vi.mock('lucide-vue-next', () => ({ Check: { template: '<i />' } }));
import ChooseFavoriteGroupDialog from '../ChooseFavoriteGroupDialog.vue'; import ChooseFavoriteGroupDialog from '../ChooseFavoriteGroupDialog.vue';
@@ -42,7 +74,12 @@ describe('ChooseFavoriteGroupDialog.vue', () => {
beforeEach(() => { beforeEach(() => {
mocks.addFavorite.mockClear(); mocks.addFavorite.mockClear();
mocks.toastSuccess.mockClear(); mocks.toastSuccess.mockClear();
mocks.favoriteDialog.value = { visible: true, type: 'friend', objectId: 'usr_1', currentGroup: null }; mocks.favoriteDialog.value = {
visible: true,
type: 'friend',
objectId: 'usr_1',
currentGroup: null
};
}); });
it('runs delete action for current group', async () => { it('runs delete action for current group', async () => {
@@ -50,7 +87,12 @@ describe('ChooseFavoriteGroupDialog.vue', () => {
visible: true, visible: true,
type: 'friend', type: 'friend',
objectId: 'usr_1', objectId: 'usr_1',
currentGroup: { key: 'group_1', displayName: 'G1', count: 0, capacity: 100 } currentGroup: {
key: 'group_1',
displayName: 'G1',
count: 0,
capacity: 100
}
}; };
const wrapper = mount(ChooseFavoriteGroupDialog); const wrapper = mount(ChooseFavoriteGroupDialog);
await wrapper.get('[data-testid="btn"]').trigger('click'); await wrapper.get('[data-testid="btn"]').trigger('click');
@@ -5,8 +5,16 @@ vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('@/shared/utils/common', () => ({ openExternalLink: vi.fn() })); vi.mock('@/shared/utils/common', () => ({ openExternalLink: vi.fn() }));
vi.mock('../../../stores', () => ({ vi.mock('../../../stores', () => ({
useDashboardStore: () => ({ useDashboardStore: () => ({
createDashboard: vi.fn(async () => ({ id: 'dashboard-1', name: 'Dashboard', icon: 'ri-dashboard-line' })), createDashboard: vi.fn(async () => ({
getDashboard: vi.fn(() => ({ id: 'dashboard-1', name: 'Dashboard', icon: 'ri-dashboard-line' })), id: 'dashboard-1',
name: 'Dashboard',
icon: 'ri-dashboard-line'
})),
getDashboard: vi.fn(() => ({
id: 'dashboard-1',
name: 'Dashboard',
icon: 'ri-dashboard-line'
})),
updateDashboard: vi.fn(async () => {}), updateDashboard: vi.fn(async () => {}),
deleteDashboard: vi.fn(async () => {}), deleteDashboard: vi.fn(async () => {}),
setEditingDashboardId: vi.fn() setEditingDashboardId: vi.fn()
@@ -25,7 +33,8 @@ vi.mock('@/components/ui/dialog', () => ({
vi.mock('@/components/ui/button', () => ({ vi.mock('@/components/ui/button', () => ({
Button: { Button: {
emits: ['click'], emits: ['click'],
template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>' template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
} }
})); }));
vi.mock('@/components/ui/hover-card', () => ({ vi.mock('@/components/ui/hover-card', () => ({
@@ -37,17 +46,26 @@ vi.mock('@/components/ui/input-group', () => ({
InputGroupButton: { template: '<button><slot /></button>' }, InputGroupButton: { template: '<button><slot /></button>' },
InputGroupField: { template: '<input />' } InputGroupField: { template: '<input />' }
})); }));
vi.mock('@/components/ui/separator', () => ({ Separator: { template: '<hr />' } })); vi.mock('@/components/ui/separator', () => ({
Separator: { template: '<hr />' }
}));
vi.mock('@/components/ui/tree', () => ({ vi.mock('@/components/ui/tree', () => ({
Tree: { Tree: {
props: ['items'], props: ['items'],
template: '<div><slot :flatten-items="[]" /></div>' template: '<div><slot :flatten-items="[]" /></div>'
} }
})); }));
vi.mock('@dnd-kit/vue', () => ({ DragDropProvider: { template: '<div><slot /></div>' } })); vi.mock('@dnd-kit/vue', () => ({
DragDropProvider: { template: '<div><slot /></div>' }
}));
vi.mock('@dnd-kit/vue/sortable', () => ({ isSortable: () => false })); vi.mock('@dnd-kit/vue/sortable', () => ({ isSortable: () => false }));
vi.mock('lucide-vue-next', () => new Proxy({}, { get: () => ({ template: '<i />' }) })); vi.mock(
vi.mock('../SortableTreeNode.vue', () => ({ default: { template: '<div />' } })); 'lucide-vue-next',
() => new Proxy({}, { get: () => ({ template: '<i />' }) })
);
vi.mock('../SortableTreeNode.vue', () => ({
default: { template: '<div />' }
}));
import CustomNavDialog from '../CustomNavDialog.vue'; import CustomNavDialog from '../CustomNavDialog.vue';
@@ -74,8 +92,12 @@ describe('CustomNavDialog.vue', () => {
}); });
const buttons = wrapper.findAll('[data-testid="btn"]'); const buttons = wrapper.findAll('[data-testid="btn"]');
const resetButton = buttons.find((button) => button.text().includes('nav_menu.custom_nav.restore_default')); const resetButton = buttons.find((button) =>
const saveButton = buttons.find((button) => button.text().includes('common.actions.confirm')); button.text().includes('nav_menu.custom_nav.restore_default')
);
const saveButton = buttons.find((button) =>
button.text().includes('common.actions.confirm')
);
await resetButton.trigger('click'); await resetButton.trigger('click');
await saveButton.trigger('click'); await saveButton.trigger('click');
@@ -19,13 +19,36 @@ vi.mock('../../composables/useImageCropper', () => ({
getCroppedBlob: (...a) => mocks.getCroppedBlob(...a) getCroppedBlob: (...a) => mocks.getCroppedBlob(...a)
}) })
})); }));
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' }, DialogHeader: { template: '<div><slot /></div>' }, DialogTitle: { template: '<div><slot /></div>' }, DialogFooter: { template: '<div><slot /></div>' } })); vi.mock('@/components/ui/dialog', () => ({
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>' } })); Dialog: { template: '<div><slot /></div>' },
vi.mock('@/components/ui/slider', () => ({ Slider: { emits: ['value-commit'], template: '<div />' } })); DialogContent: { template: '<div><slot /></div>' },
vi.mock('@/components/ui/spinner', () => ({ Spinner: { template: '<div />' } })); DialogHeader: { template: '<div><slot /></div>' },
vi.mock('@/components/ui/tooltip/TooltipWrapper.vue', () => ({ default: { template: '<div><slot /></div>' } })); DialogTitle: { template: '<div><slot /></div>' },
vi.mock('vue-advanced-cropper', () => ({ Cropper: { emits: ['change'], template: '<div />' } })); DialogFooter: { template: '<div><slot /></div>' }
vi.mock('lucide-vue-next', () => new Proxy({}, { get: () => ({ template: '<i />' }) })); }));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('@/components/ui/slider', () => ({
Slider: { emits: ['value-commit'], template: '<div />' }
}));
vi.mock('@/components/ui/spinner', () => ({
Spinner: { template: '<div />' }
}));
vi.mock('@/components/ui/tooltip/TooltipWrapper.vue', () => ({
default: { template: '<div><slot /></div>' }
}));
vi.mock('vue-advanced-cropper', () => ({
Cropper: { emits: ['change'], template: '<div />' }
}));
vi.mock(
'lucide-vue-next',
() => new Proxy({}, { get: () => ({ template: '<i />' }) })
);
import ImageCropDialog from '../ImageCropDialog.vue'; import ImageCropDialog from '../ImageCropDialog.vue';
@@ -10,24 +10,71 @@ const mocks = vi.hoisted(() => ({
setString: vi.fn(), setString: vi.fn(),
getString: vi.fn(async () => ''), getString: vi.fn(async () => ''),
applyGroup: vi.fn((g) => g), applyGroup: vi.fn((g) => g),
inviteDialog: { __v_isRef: true, value: { visible: true, loading: false, groupId: 'grp_1', userId: '', userIds: ['usr_1'], groupName: '', userObject: null } } inviteDialog: {
__v_isRef: true,
value: {
visible: true,
loading: false,
groupId: 'grp_1',
userId: '',
userIds: ['usr_1'],
groupName: '',
userObject: null
}
}
})); }));
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s })); vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) })); vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('vue-sonner', () => ({ toast: { error: vi.fn() } })); vi.mock('vue-sonner', () => ({ toast: { error: vi.fn() } }));
vi.mock('../../../shared/utils', () => ({ hasGroupPermission: () => true })); vi.mock('../../../shared/utils', () => ({ hasGroupPermission: () => true }));
vi.mock('../../../composables/useUserDisplay', () => ({ useUserDisplay: () => ({ userImage: () => '', userStatusClass: () => '' }) })); vi.mock('../../../composables/useUserDisplay', () => ({
useUserDisplay: () => ({ userImage: () => '', userStatusClass: () => '' })
}));
vi.mock('../../../stores', () => ({ vi.mock('../../../stores', () => ({
useFriendStore: () => ({ vipFriends: ref([]), onlineFriends: ref([]), activeFriends: ref([]), offlineFriends: ref([]) }), useFriendStore: () => ({
useGroupStore: () => ({ currentUserGroups: ref(new Map()), inviteGroupDialog: mocks.inviteDialog, applyGroup: (...a) => mocks.applyGroup(...a) }), vipFriends: ref([]),
onlineFriends: ref([]),
activeFriends: ref([]),
offlineFriends: ref([])
}),
useGroupStore: () => ({
currentUserGroups: ref(new Map()),
inviteGroupDialog: mocks.inviteDialog,
applyGroup: (...a) => mocks.applyGroup(...a)
}),
useModalStore: () => ({ confirm: (...a) => mocks.confirm(...a) }) useModalStore: () => ({ confirm: (...a) => mocks.confirm(...a) })
})); }));
vi.mock('../../../api', () => ({ groupRequest: { sendGroupInvite: (...a) => mocks.sendGroupInvite(...a), getGroup: (...a) => mocks.getGroup(...a) }, queryRequest: { fetch: (...a) => mocks.fetch(...a) } })); vi.mock('../../../api', () => ({
vi.mock('../../../services/config', () => ({ default: { getString: (...a) => mocks.getString(...a), setString: (...a) => mocks.setString(...a) } })); groupRequest: {
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' }, DialogHeader: { template: '<div><slot /></div>' }, DialogTitle: { template: '<div><slot /></div>' }, DialogFooter: { template: '<div><slot /></div>' } })); sendGroupInvite: (...a) => mocks.sendGroupInvite(...a),
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>' } })); getGroup: (...a) => mocks.getGroup(...a)
vi.mock('../../ui/virtual-combobox', () => ({ VirtualCombobox: { template: '<div />' } })); },
queryRequest: { fetch: (...a) => mocks.fetch(...a) }
}));
vi.mock('../../../services/config', () => ({
default: {
getString: (...a) => mocks.getString(...a),
setString: (...a) => mocks.setString(...a)
}
}));
vi.mock('@/components/ui/dialog', () => ({
Dialog: { template: '<div><slot /></div>' },
DialogContent: { template: '<div><slot /></div>' },
DialogHeader: { template: '<div><slot /></div>' },
DialogTitle: { template: '<div><slot /></div>' },
DialogFooter: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('../../ui/virtual-combobox', () => ({
VirtualCombobox: { template: '<div />' }
}));
vi.mock('lucide-vue-next', () => ({ Check: { template: '<i />' } })); vi.mock('lucide-vue-next', () => ({ Check: { template: '<i />' } }));
import InviteGroupDialog from '../InviteGroupDialog.vue'; import InviteGroupDialog from '../InviteGroupDialog.vue';
@@ -6,10 +6,19 @@ const mocks = vi.hoisted(() => ({
selfInvite: vi.fn(async () => ({})), selfInvite: vi.fn(async () => ({})),
writeText: vi.fn(), writeText: vi.fn(),
getBool: vi.fn(async () => false), getBool: vi.fn(async () => false),
launchDialogData: { value: { visible: true, loading: true, tag: 'wrld_1:123', shortName: 'abc' } } launchDialogData: {
value: {
visible: true,
loading: true,
tag: 'wrld_1:123',
shortName: 'abc'
}
}
})); }));
Object.assign(globalThis, { navigator: { clipboard: { writeText: (...a) => mocks.writeText(...a) } } }); Object.assign(globalThis, {
navigator: { clipboard: { writeText: (...a) => mocks.writeText(...a) } }
});
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s })); vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) })); vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
@@ -18,27 +27,81 @@ vi.mock('../../../stores', () => ({
useFriendStore: () => ({ friends: ref(new Map()) }), useFriendStore: () => ({ friends: ref(new Map()) }),
useGameStore: () => ({ isGameRunning: ref(false) }), useGameStore: () => ({ isGameRunning: ref(false) }),
useInviteStore: () => ({ canOpenInstanceInGame: ref(false) }), useInviteStore: () => ({ canOpenInstanceInGame: ref(false) }),
useLaunchStore: () => ({ launchDialogData: mocks.launchDialogData, launchGame: vi.fn(), tryOpenInstanceInVrc: vi.fn() }), useLaunchStore: () => ({
launchDialogData: mocks.launchDialogData,
launchGame: vi.fn(),
tryOpenInstanceInVrc: vi.fn()
}),
useLocationStore: () => ({ lastLocation: ref({ friendList: new Map() }) }), useLocationStore: () => ({ lastLocation: ref({ friendList: new Map() }) }),
useModalStore: () => ({ confirm: vi.fn() }) useModalStore: () => ({ confirm: vi.fn() })
})); }));
vi.mock('../../../shared/utils', () => ({ vi.mock('../../../shared/utils', () => ({
getLaunchURL: () => 'vrchat://launch', getLaunchURL: () => 'vrchat://launch',
isRealInstance: () => true, isRealInstance: () => true,
parseLocation: () => ({ isRealInstance: true, worldId: 'wrld_1', instanceId: '123', tag: 'wrld_1:123' }) parseLocation: () => ({
isRealInstance: true,
worldId: 'wrld_1',
instanceId: '123',
tag: 'wrld_1:123'
})
}));
vi.mock('../../../composables/useInviteChecks', () => ({
useInviteChecks: () => ({ checkCanInvite: () => true })
}));
vi.mock('../../../api', () => ({
instanceRequest: {
selfInvite: (...a) => mocks.selfInvite(...a),
getInstanceShortName: vi.fn()
},
queryRequest: { fetch: vi.fn() }
}));
vi.mock('../../../services/config', () => ({
default: { getBool: (...a) => mocks.getBool(...a), setBool: vi.fn() }
}));
vi.mock('@/components/ui/dialog', () => ({
Dialog: { template: '<div><slot /></div>' },
DialogContent: { template: '<div><slot /></div>' },
DialogHeader: { template: '<div><slot /></div>' },
DialogTitle: { template: '<div><slot /></div>' },
DialogDescription: { template: '<div><slot /></div>' },
DialogFooter: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenu: { template: '<div><slot /></div>' },
DropdownMenuTrigger: { template: '<div><slot /></div>' },
DropdownMenuContent: { template: '<div><slot /></div>' },
DropdownMenuItem: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/field', () => ({
Field: { template: '<div><slot /></div>' },
FieldGroup: { template: '<div><slot /></div>' },
FieldLabel: { template: '<div><slot /></div>' },
FieldContent: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('@/components/ui/button-group', () => ({
ButtonGroup: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/input-group', () => ({
InputGroupField: { template: '<input />' }
}));
vi.mock('@/components/ui/tooltip', () => ({
TooltipWrapper: { template: '<div><slot /></div>' }
}));
vi.mock('../InviteDialog/InviteDialog.vue', () => ({
default: { template: '<div />' }
}));
vi.mock('lucide-vue-next', () => ({
Copy: { template: '<i />' },
Info: { template: '<i />' },
MoreHorizontal: { template: '<i />' }
})); }));
vi.mock('../../../composables/useInviteChecks', () => ({ useInviteChecks: () => ({ checkCanInvite: () => true }) }));
vi.mock('../../../api', () => ({ instanceRequest: { selfInvite: (...a) => mocks.selfInvite(...a), getInstanceShortName: vi.fn() }, queryRequest: { fetch: vi.fn() } }));
vi.mock('../../../services/config', () => ({ default: { getBool: (...a) => mocks.getBool(...a), setBool: vi.fn() } }));
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' }, DialogHeader: { template: '<div><slot /></div>' }, DialogTitle: { template: '<div><slot /></div>' }, DialogDescription: { template: '<div><slot /></div>' }, DialogFooter: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/dropdown-menu', () => ({ DropdownMenu: { template: '<div><slot /></div>' }, DropdownMenuTrigger: { template: '<div><slot /></div>' }, DropdownMenuContent: { template: '<div><slot /></div>' }, DropdownMenuItem: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/field', () => ({ Field: { template: '<div><slot /></div>' }, FieldGroup: { template: '<div><slot /></div>' }, FieldLabel: { template: '<div><slot /></div>' }, FieldContent: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>' } }));
vi.mock('@/components/ui/button-group', () => ({ ButtonGroup: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/input-group', () => ({ InputGroupField: { template: '<input />' } }));
vi.mock('@/components/ui/tooltip', () => ({ TooltipWrapper: { template: '<div><slot /></div>' } }));
vi.mock('../InviteDialog/InviteDialog.vue', () => ({ default: { template: '<div />' } }));
vi.mock('lucide-vue-next', () => ({ Copy: { template: '<i />' }, Info: { template: '<i />' }, MoreHorizontal: { template: '<i />' } }));
import LaunchDialog from '../LaunchDialog.vue'; import LaunchDialog from '../LaunchDialog.vue';
@@ -5,31 +5,83 @@ import { ref } from 'vue';
const mocks = vi.hoisted(() => ({ const mocks = vi.hoisted(() => ({
closeMainDialog: vi.fn(), closeMainDialog: vi.fn(),
handleBreadcrumbClick: vi.fn(), handleBreadcrumbClick: vi.fn(),
dialogCrumbs: { value: [{ type: 'user', id: 'u1', label: 'User' }, { type: 'world', id: 'w1', label: 'World' }] }, dialogCrumbs: {
value: [
{ type: 'user', id: 'u1', label: 'User' },
{ type: 'world', id: 'w1', label: 'World' }
]
},
userVisible: { value: true } userVisible: { value: true }
})); }));
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s })); vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('@/stores', () => ({ vi.mock('@/stores', () => ({
useUiStore: () => ({ dialogCrumbs: mocks.dialogCrumbs.value, closeMainDialog: (...a) => mocks.closeMainDialog(...a), handleBreadcrumbClick: (...a) => mocks.handleBreadcrumbClick(...a) }), useUiStore: () => ({
dialogCrumbs: mocks.dialogCrumbs.value,
closeMainDialog: (...a) => mocks.closeMainDialog(...a),
handleBreadcrumbClick: (...a) => mocks.handleBreadcrumbClick(...a)
}),
useUserStore: () => ({ userDialog: { visible: mocks.userVisible.value } }), useUserStore: () => ({ userDialog: { visible: mocks.userVisible.value } }),
useWorldStore: () => ({ worldDialog: { visible: false } }), useWorldStore: () => ({ worldDialog: { visible: false } }),
useAvatarStore: () => ({ avatarDialog: { visible: false } }), useAvatarStore: () => ({ avatarDialog: { visible: false } }),
useGroupStore: () => ({ groupDialog: { visible: false } }), useGroupStore: () => ({ groupDialog: { visible: false } }),
useInstanceStore: () => ({ previousInstancesInfoDialog: ref({ visible: false }), previousInstancesListDialog: ref({ visible: false, variant: 'user' }) }) useInstanceStore: () => ({
previousInstancesInfoDialog: ref({ visible: false }),
previousInstancesListDialog: ref({ visible: false, variant: 'user' })
})
}));
vi.mock('@/components/ui/dialog', () => ({
Dialog: { template: '<div><slot /></div>' },
DialogContent: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/breadcrumb', () => ({
Breadcrumb: { template: '<div><slot /></div>' },
BreadcrumbList: { template: '<div><slot /></div>' },
BreadcrumbItem: { template: '<div><slot /></div>' },
BreadcrumbLink: { template: '<div><slot /></div>' },
BreadcrumbSeparator: { template: '<span>/</span>' },
BreadcrumbPage: { template: '<span><slot /></span>' },
BreadcrumbEllipsis: { template: '<span>...</span>' }
}));
vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenu: { template: '<div><slot /></div>' },
DropdownMenuTrigger: { template: '<div><slot /></div>' },
DropdownMenuContent: { template: '<div><slot /></div>' },
DropdownMenuItem: {
emits: ['click'],
template:
'<button data-testid="crumb-dd" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('@/components/ui/tooltip', () => ({
TooltipWrapper: { template: '<div><slot /></div>' }
})); }));
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/breadcrumb', () => ({ Breadcrumb: { template: '<div><slot /></div>' }, BreadcrumbList: { template: '<div><slot /></div>' }, BreadcrumbItem: { template: '<div><slot /></div>' }, BreadcrumbLink: { template: '<div><slot /></div>' }, BreadcrumbSeparator: { template: '<span>/</span>' }, BreadcrumbPage: { template: '<span><slot /></span>' }, BreadcrumbEllipsis: { template: '<span>...</span>' } }));
vi.mock('@/components/ui/dropdown-menu', () => ({ DropdownMenu: { template: '<div><slot /></div>' }, DropdownMenuTrigger: { template: '<div><slot /></div>' }, DropdownMenuContent: { template: '<div><slot /></div>' }, DropdownMenuItem: { emits: ['click'], template: '<button data-testid="crumb-dd" @click="$emit(\'click\')"><slot /></button>' } }));
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>' } }));
vi.mock('@/components/ui/tooltip', () => ({ TooltipWrapper: { template: '<div><slot /></div>' } }));
vi.mock('lucide-vue-next', () => ({ ArrowLeft: { template: '<i />' } })); vi.mock('lucide-vue-next', () => ({ ArrowLeft: { template: '<i />' } }));
vi.mock('../AvatarDialog/AvatarDialog.vue', () => ({ default: { template: '<div />' } })); vi.mock('../AvatarDialog/AvatarDialog.vue', () => ({
vi.mock('../GroupDialog/GroupDialog.vue', () => ({ default: { template: '<div />' } })); default: { template: '<div />' }
vi.mock('../PreviousInstancesDialog/PreviousInstancesInfoDialog.vue', () => ({ default: { template: '<div />' } })); }));
vi.mock('../PreviousInstancesDialog/PreviousInstancesListDialog.vue', () => ({ default: { template: '<div />' } })); vi.mock('../GroupDialog/GroupDialog.vue', () => ({
vi.mock('../UserDialog/UserDialog.vue', () => ({ default: { template: '<div data-testid="user-dialog" />' } })); default: { template: '<div />' }
vi.mock('../WorldDialog/WorldDialog.vue', () => ({ default: { template: '<div />' } })); }));
vi.mock('../PreviousInstancesDialog/PreviousInstancesInfoDialog.vue', () => ({
default: { template: '<div />' }
}));
vi.mock('../PreviousInstancesDialog/PreviousInstancesListDialog.vue', () => ({
default: { template: '<div />' }
}));
vi.mock('../UserDialog/UserDialog.vue', () => ({
default: { template: '<div data-testid="user-dialog" />' }
}));
vi.mock('../WorldDialog/WorldDialog.vue', () => ({
default: { template: '<div />' }
}));
import MainDialogContainer from '../MainDialogContainer.vue'; import MainDialogContainer from '../MainDialogContainer.vue';
@@ -9,16 +9,47 @@ const mocks = vi.hoisted(() => ({
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s })); vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) })); vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('../../../api', () => ({ miscRequest: { sendBoop: (...a) => mocks.sendBoop(...a) }, notificationRequest: { hideNotificationV2: vi.fn() }, queryRequest: { fetch: (...a) => mocks.fetch(...a) } })); vi.mock('../../../api', () => ({
vi.mock('../../../stores', () => ({ miscRequest: { sendBoop: (...a) => mocks.sendBoop(...a) },
useUserStore: () => ({ sendBoopDialog: mocks.boopDialog, isLocalUserVrcPlusSupporter: { value: false } }), notificationRequest: { hideNotificationV2: vi.fn() },
useNotificationStore: () => ({ notificationTable: { value: { data: [] } }, isNotificationExpired: () => false, handleNotificationV2Hide: vi.fn() }), queryRequest: { fetch: (...a) => mocks.fetch(...a) }
useGalleryStore: () => ({ showGalleryPage: vi.fn(), refreshEmojiTable: vi.fn(), emojiTable: { value: [] } }) }));
vi.mock('../../../stores', () => ({
useUserStore: () => ({
sendBoopDialog: mocks.boopDialog,
isLocalUserVrcPlusSupporter: { value: false }
}),
useNotificationStore: () => ({
notificationTable: { value: { data: [] } },
isNotificationExpired: () => false,
handleNotificationV2Hide: vi.fn()
}),
useGalleryStore: () => ({
showGalleryPage: vi.fn(),
refreshEmojiTable: vi.fn(),
emojiTable: { value: [] }
})
}));
vi.mock('../../../shared/constants/photon.js', () => ({
photonEmojis: ['Wave']
}));
vi.mock('@/components/ui/dialog', () => ({
Dialog: { template: '<div><slot /></div>' },
DialogContent: { template: '<div><slot /></div>' },
DialogHeader: { template: '<div><slot /></div>' },
DialogTitle: { template: '<div><slot /></div>' },
DialogFooter: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('../../ui/virtual-combobox', () => ({
VirtualCombobox: { template: '<div />' }
})); }));
vi.mock('../../../shared/constants/photon.js', () => ({ photonEmojis: ['Wave'] }));
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' }, DialogHeader: { template: '<div><slot /></div>' }, DialogTitle: { template: '<div><slot /></div>' }, DialogFooter: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>' } }));
vi.mock('../../ui/virtual-combobox', () => ({ VirtualCombobox: { template: '<div />' } }));
vi.mock('../../Emoji.vue', () => ({ default: { template: '<div />' } })); vi.mock('../../Emoji.vue', () => ({ default: { template: '<div />' } }));
vi.mock('lucide-vue-next', () => ({ Check: { template: '<i />' } })); vi.mock('lucide-vue-next', () => ({ Check: { template: '<i />' } }));
@@ -4,7 +4,9 @@ import { mount } from '@vue/test-utils';
const mocks = vi.hoisted(() => ({ const mocks = vi.hoisted(() => ({
close: vi.fn(), close: vi.fn(),
save: vi.fn(), save: vi.fn(),
dialog: { value: { visible: true, maxTableSize: '1000', searchLimit: '100' } } dialog: {
value: { visible: true, maxTableSize: '1000', searchLimit: '100' }
}
})); }));
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s })); vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
@@ -20,10 +22,30 @@ vi.mock('../../../stores', () => ({
SEARCH_LIMIT_MAX: 1000 SEARCH_LIMIT_MAX: 1000
}) })
})); }));
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' }, DialogHeader: { template: '<div><slot /></div>' }, DialogTitle: { template: '<div><slot /></div>' }, DialogDescription: { template: '<div><slot /></div>' }, DialogFooter: { template: '<div><slot /></div>' } })); vi.mock('@/components/ui/dialog', () => ({
vi.mock('@/components/ui/field', () => ({ Field: { template: '<div><slot /></div>' }, FieldGroup: { template: '<div><slot /></div>' }, FieldLabel: { template: '<div><slot /></div>' }, FieldContent: { template: '<div><slot /></div>' } })); Dialog: { template: '<div><slot /></div>' },
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" :disabled="$attrs.disabled" @click="$emit(\'click\')"><slot /></button>' } })); DialogContent: { template: '<div><slot /></div>' },
vi.mock('@/components/ui/input-group', () => ({ InputGroupField: { template: '<input />' } })); DialogHeader: { template: '<div><slot /></div>' },
DialogTitle: { template: '<div><slot /></div>' },
DialogDescription: { template: '<div><slot /></div>' },
DialogFooter: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/field', () => ({
Field: { template: '<div><slot /></div>' },
FieldGroup: { template: '<div><slot /></div>' },
FieldLabel: { template: '<div><slot /></div>' },
FieldContent: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" :disabled="$attrs.disabled" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('@/components/ui/input-group', () => ({
InputGroupField: { template: '<input />' }
}));
import TableLimitsDialog from '../TableLimitsDialog.vue'; import TableLimitsDialog from '../TableLimitsDialog.vue';
+1 -3
View File
@@ -326,9 +326,7 @@
const dashboard = await dashboardStore.createDashboard(t('dashboard.default_name')); const dashboard = await dashboardStore.createDashboard(t('dashboard.default_name'));
const dashboardKey = `${DASHBOARD_NAV_KEY_PREFIX}${dashboard.id}`; const dashboardKey = `${DASHBOARD_NAV_KEY_PREFIX}${dashboard.id}`;
const currentLayout = [...navLayout.value]; const currentLayout = [...navLayout.value];
const directAccessIdx = currentLayout.findIndex( const directAccessIdx = currentLayout.findIndex((entry) => entry.type === 'item' && entry.key === 'direct-access');
(entry) => entry.type === 'item' && entry.key === 'direct-access'
);
const newEntry = { type: 'item', key: dashboardKey }; const newEntry = { type: 'item', key: dashboardKey };
if (directAccessIdx !== -1) { if (directAccessIdx !== -1) {
currentLayout.splice(directAccessIdx, 0, newEntry); currentLayout.splice(directAccessIdx, 0, newEntry);
@@ -13,12 +13,15 @@ vi.mock('lucide-vue-next', () => ({
vi.mock('@/components/ui/sidebar', () => ({ vi.mock('@/components/ui/sidebar', () => ({
SidebarMenuItem: { template: '<div><slot /></div>' }, SidebarMenuItem: { template: '<div><slot /></div>' },
SidebarMenuButton: { template: '<button data-testid="folder-btn"><slot /></button>' }, SidebarMenuButton: {
template: '<button data-testid="folder-btn"><slot /></button>'
},
SidebarMenuSub: { template: '<div><slot /></div>' }, SidebarMenuSub: { template: '<div><slot /></div>' },
SidebarMenuSubItem: { template: '<div><slot /></div>' }, SidebarMenuSubItem: { template: '<div><slot /></div>' },
SidebarMenuSubButton: { SidebarMenuSubButton: {
emits: ['click'], emits: ['click'],
template: '<button data-testid="submenu-btn" @click="$emit(\'click\')"><slot /></button>' template:
'<button data-testid="submenu-btn" @click="$emit(\'click\')"><slot /></button>'
} }
})); }));
@@ -32,18 +35,25 @@ vi.mock('@/components/ui/context-menu', () => ({
ContextMenu: { template: '<div><slot /></div>' }, ContextMenu: { template: '<div><slot /></div>' },
ContextMenuTrigger: { template: '<div><slot /></div>' }, ContextMenuTrigger: { template: '<div><slot /></div>' },
ContextMenuContent: { template: '<div><slot /></div>' }, ContextMenuContent: { template: '<div><slot /></div>' },
ContextMenuItem: { emits: ['click'], template: '<button @click="$emit(\'click\')"><slot /></button>' }, ContextMenuItem: {
emits: ['click'],
template: '<button @click="$emit(\'click\')"><slot /></button>'
},
ContextMenuSeparator: { template: '<div />' } ContextMenuSeparator: { template: '<div />' }
})); }));
vi.mock('@/components/ui/dropdown-menu', () => ({ vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenu: { DropdownMenu: {
emits: ['update:open'], emits: ['update:open'],
template: '<div><button data-testid="dropdown-open" @click="$emit(\'update:open\', true)" /><slot /></div>' template:
'<div><button data-testid="dropdown-open" @click="$emit(\'update:open\', true)" /><slot /></div>'
}, },
DropdownMenuTrigger: { template: '<div><slot /></div>' }, DropdownMenuTrigger: { template: '<div><slot /></div>' },
DropdownMenuContent: { template: '<div><slot /></div>' }, DropdownMenuContent: { template: '<div><slot /></div>' },
DropdownMenuItem: { emits: ['select'], template: '<button @click="$emit(\'select\', $event)"><slot /></button>' } DropdownMenuItem: {
emits: ['select'],
template: '<button @click="$emit(\'select\', $event)"><slot /></button>'
}
})); }));
import NavMenuFolderItem from '../NavMenuFolderItem.vue'; import NavMenuFolderItem from '../NavMenuFolderItem.vue';
@@ -53,7 +63,14 @@ const folderItem = {
icon: 'ri-folder-line', icon: 'ri-folder-line',
title: 'Folder', title: 'Folder',
titleIsCustom: true, titleIsCustom: true,
children: [{ index: 'feed', label: 'nav_tooltip.feed', icon: 'ri-rss-line', titleIsCustom: false }] children: [
{
index: 'feed',
label: 'nav_tooltip.feed',
icon: 'ri-rss-line',
titleIsCustom: false
}
]
}; };
describe('NavMenuFolderItem', () => { describe('NavMenuFolderItem', () => {
@@ -21,7 +21,8 @@ vi.mock('@/components/ui/sidebar', () => ({
SidebarMenuItem: { template: '<div><slot /></div>' }, SidebarMenuItem: { template: '<div><slot /></div>' },
SidebarMenuButton: { SidebarMenuButton: {
emits: ['click'], emits: ['click'],
template: '<button data-testid="sidebar-menu-btn" @click="$emit(\'click\')"><slot /></button>' template:
'<button data-testid="sidebar-menu-btn" @click="$emit(\'click\')"><slot /></button>'
} }
})); }));
@@ -31,7 +32,8 @@ vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenuContent: { template: '<div><slot /></div>' }, DropdownMenuContent: { template: '<div><slot /></div>' },
DropdownMenuItem: { DropdownMenuItem: {
emits: ['click', 'select'], emits: ['click', 'select'],
template: '<button data-testid="dd-item" @click="$emit(\'click\')" @mousedown="$emit(\'select\')"><slot /></button>' template:
'<button data-testid="dd-item" @click="$emit(\'click\')" @mousedown="$emit(\'select\')"><slot /></button>'
}, },
DropdownMenuLabel: { template: '<div><slot /></div>' }, DropdownMenuLabel: { template: '<div><slot /></div>' },
DropdownMenuSeparator: { template: '<div />' }, DropdownMenuSeparator: { template: '<div />' },
@@ -40,7 +42,8 @@ vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenuSubContent: { template: '<div><slot /></div>' }, DropdownMenuSubContent: { template: '<div><slot /></div>' },
DropdownMenuCheckboxItem: { DropdownMenuCheckboxItem: {
emits: ['select'], emits: ['select'],
template: '<button data-testid="dd-check" @click="$emit(\'select\')"><slot /></button>' template:
'<button data-testid="dd-check" @click="$emit(\'select\')"><slot /></button>'
} }
})); }));
@@ -71,7 +71,10 @@ describe('useNavLayout', () => {
triggerNavAction({ routeName: 'dashboard', routeParams: { id: '1' } }); triggerNavAction({ routeName: 'dashboard', routeParams: { id: '1' } });
expect(deps.push).toHaveBeenCalledWith({ name: 'dashboard', params: { id: '1' } }); expect(deps.push).toHaveBeenCalledWith({
name: 'dashboard',
params: { id: '1' }
});
}); });
it('applies custom layout and persists', async () => { it('applies custom layout and persists', async () => {
@@ -22,7 +22,11 @@ describe('useNavTheme', () => {
const setTableDensity = vi.fn(); const setTableDensity = vi.fn();
const toggleThemeMode = vi.fn(); const toggleThemeMode = vi.fn();
const { handleThemeSelect, handleTableDensitySelect, handleThemeToggle } = useNavTheme({ const {
handleThemeSelect,
handleTableDensitySelect,
handleThemeToggle
} = useNavTheme({
t: (key) => key, t: (key) => key,
appearanceSettingsStore: { appearanceSettingsStore: {
setThemeMode, setThemeMode,
+2 -8
View File
@@ -69,17 +69,11 @@
createYearRange({ createYearRange({
start: start:
props?.minValue ?? props?.minValue ??
(toRaw(props.placeholder) ?? props.defaultPlaceholder ?? today(getLocalTimeZone())).cycle( (toRaw(props.placeholder) ?? props.defaultPlaceholder ?? today(getLocalTimeZone())).cycle('year', -100),
'year',
-100
),
end: end:
props?.maxValue ?? props?.maxValue ??
(toRaw(props.placeholder) ?? props.defaultPlaceholder ?? today(getLocalTimeZone())).cycle( (toRaw(props.placeholder) ?? props.defaultPlaceholder ?? today(getLocalTimeZone())).cycle('year', 10)
'year',
10
)
}) })
); );
}); });
@@ -27,13 +27,7 @@
as: { type: null, required: false }, as: { type: null, required: false },
class: { type: null, required: false } class: { type: null, required: false }
}); });
const emits = defineEmits([ const emits = defineEmits(['escapeKeyDown', 'pointerDownOutside', 'focusOutside', 'interactOutside', 'closeAutoFocus']);
'escapeKeyDown',
'pointerDownOutside',
'focusOutside',
'interactOutside',
'closeAutoFocus'
]);
const delegatedProps = reactiveOmit(props, 'class'); const delegatedProps = reactiveOmit(props, 'class');
@@ -31,13 +31,7 @@
as: { type: null, required: false }, as: { type: null, required: false },
class: { type: null, required: false } class: { type: null, required: false }
}); });
const emits = defineEmits([ const emits = defineEmits(['escapeKeyDown', 'pointerDownOutside', 'focusOutside', 'interactOutside', 'closeAutoFocus']);
'escapeKeyDown',
'pointerDownOutside',
'focusOutside',
'interactOutside',
'closeAutoFocus'
]);
const delegatedProps = reactiveOmit(props, 'class'); const delegatedProps = reactiveOmit(props, 'class');
@@ -50,12 +50,7 @@
class: { type: null, required: false } class: { type: null, required: false }
}); });
const emits = defineEmits([ const emits = defineEmits(['update:modelValue', 'update:validModelValue', 'update:placeholder', 'update:startValue']);
'update:modelValue',
'update:validModelValue',
'update:placeholder',
'update:startValue'
]);
const delegatedProps = reactiveOmit(props, 'class'); const delegatedProps = reactiveOmit(props, 'class');
@@ -126,9 +126,7 @@
const groupItems = Array.isArray(group?.items) ? group.items : []; const groupItems = Array.isArray(group?.items) ? group.items : [];
const filteredItems = normalizedSearch const filteredItems = normalizedSearch
? groupItems.filter((item) => ? groupItems.filter((item) => (item.search ?? item.label ?? '').toLowerCase().includes(normalizedSearch))
(item.search ?? item.label ?? '').toLowerCase().includes(normalizedSearch)
)
: groupItems; : groupItems;
if (!filteredItems.length) continue; if (!filteredItems.length) continue;
@@ -5,9 +5,7 @@ vi.mock('vue-sonner', () => ({
})); }));
vi.mock('vue-i18n', () => ({ vi.mock('vue-i18n', () => ({
useI18n: () => ({ t: (key) => key , useI18n: () => ({ t: (key) => key, locale: require('vue').ref('en') })
locale: require('vue').ref('en')
})
})); }));
import { import {
+10 -2
View File
@@ -48,8 +48,16 @@ describe('entity query cache helpers', () => {
}; };
}); });
const first = await fetchWithEntityPolicy({ queryKey, policy, queryFn }); const first = await fetchWithEntityPolicy({
const second = await fetchWithEntityPolicy({ queryKey, policy, queryFn }); queryKey,
policy,
queryFn
});
const second = await fetchWithEntityPolicy({
queryKey,
policy,
queryFn
});
expect(first.cache).toBe(false); expect(first.cache).toBe(false);
expect(second.cache).toBe(true); expect(second.cache).toBe(true);
+12 -2
View File
@@ -143,7 +143,12 @@ export function patchQueryDataWithRecency({ queryKey, nextData }) {
* @param {{queryKey: unknown[], policy: {staleTime: number, gcTime: number, retry: number, refetchOnWindowFocus: boolean}, queryFn: () => Promise<any>, label?: string}} options * @param {{queryKey: unknown[], policy: {staleTime: number, gcTime: number, retry: number, refetchOnWindowFocus: boolean}, queryFn: () => Promise<any>, label?: string}} options
* @returns {Promise<{data: any, cache: boolean}>} * @returns {Promise<{data: any, cache: boolean}>}
*/ */
export async function fetchWithEntityPolicy({ queryKey, policy, queryFn, label }) { export async function fetchWithEntityPolicy({
queryKey,
policy,
queryFn,
label
}) {
const queryState = queryClient.getQueryState(queryKey); const queryState = queryClient.getQueryState(queryKey);
const isFresh = const isFresh =
Boolean(queryState?.dataUpdatedAt) && Boolean(queryState?.dataUpdatedAt) &&
@@ -157,7 +162,12 @@ export async function fetchWithEntityPolicy({ queryKey, policy, queryFn, label }
}); });
if (isFresh) { if (isFresh) {
logWebRequest('[QUERY CACHE HIT]', label || queryKey[0], queryKey, data); logWebRequest(
'[QUERY CACHE HIT]',
label || queryKey[0],
queryKey,
data
);
} else { } else {
logWebRequest('[QUERY FETCH]', label || queryKey[0], queryKey, data); logWebRequest('[QUERY FETCH]', label || queryKey[0], queryKey, data);
} }
+7 -1
View File
@@ -13,7 +13,13 @@ export const queryKeys = Object.freeze({
'member', 'member',
userId userId
], ],
groupMembers: ({ groupId, n = 100, offset = 0, sort = '', roleId = '' } = {}) => [ groupMembers: ({
groupId,
n = 100,
offset = 0,
sort = '',
roleId = ''
} = {}) => [
'group', 'group',
groupId, groupId,
'members', 'members',
+2 -2
View File
@@ -518,7 +518,7 @@
"ram": "memory,electronics,ddr,storage,dram,ddr4,ddr5,内存条,记忆卡,存储器,内存,DDR,DRAM,内存模块", "ram": "memory,electronics,ddr,storage,dram,ddr4,ddr5,内存条,记忆卡,存储器,内存,DDR,DRAM,内存模块",
"ram-2": "memory,electronics,ddr,storage,dram,ddr4,ddr5,内存条,记忆卡,存储器,内存,DDR,DRAM,内存模块" "ram-2": "memory,electronics,ddr,storage,dram,ddr4,ddr5,内存条,记忆卡,存储器,内存,DDR,DRAM,内存模块"
}, },
"Document": { "Document": {
"file": "doc,paper,new,create,record,文档,文件,新建,创建,档案,文稿", "file": "doc,paper,new,create,record,文档,文件,新建,创建,档案,文稿",
"file-ai": "artificial intelligence,smart document,ai file,automated,assistant,生成式,文件,文档,人工智能,智能文档,AI文件,自动化,助手", "file-ai": "artificial intelligence,smart document,ai file,automated,assistant,生成式,文件,文档,人工智能,智能文档,AI文件,自动化,助手",
"file-2": "doc,paper,new,create,record,文档,文件,新建,创建,档案,文稿", "file-2": "doc,paper,new,create,record,文档,文件,新建,创建,档案,文稿",
@@ -1680,4 +1680,4 @@
"book-shelf": "books,library,categories,bookshelf,书架,书籍,图书馆,分类", "book-shelf": "books,library,categories,bookshelf,书架,书籍,图书馆,分类",
"target": "aim,goal,objective,bullseye,目标,瞄准,靶心" "target": "aim,goal,objective,bullseye,目标,瞄准,靶心"
} }
} }
@@ -47,7 +47,9 @@ describe('appActions utils', () => {
let consoleErrorSpy; let consoleErrorSpy;
beforeEach(() => { beforeEach(() => {
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
vi.clearAllMocks(); vi.clearAllMocks();
mocks.searchStore.directAccessParse.mockReturnValue(false); mocks.searchStore.directAccessParse.mockReturnValue(false);
mocks.modalStore.confirm.mockResolvedValue({ ok: false }); mocks.modalStore.confirm.mockResolvedValue({ ok: false });
@@ -131,7 +133,9 @@ describe('appActions utils', () => {
test('openDiscordProfile validates empty discord id', () => { test('openDiscordProfile validates empty discord id', () => {
openDiscordProfile(''); openDiscordProfile('');
expect(mocks.toast.error).toHaveBeenCalledWith('No Discord ID provided!'); expect(mocks.toast.error).toHaveBeenCalledWith(
'No Discord ID provided!'
);
}); });
test('openDiscordProfile shows error toast when api fails', async () => { test('openDiscordProfile shows error toast when api fails', async () => {
+64 -33
View File
@@ -43,7 +43,9 @@ describe('Invite Utils', () => {
}); });
test('returns true for public instance', () => { test('returns true for public instance', () => {
expect(checkCanInvite('wrld_123:instance', defaultInviteDeps)).toBe(true); expect(checkCanInvite('wrld_123:instance', defaultInviteDeps)).toBe(
true
);
}); });
test('returns true for group instance', () => { test('returns true for group instance', () => {
@@ -56,45 +58,61 @@ describe('Invite Utils', () => {
}); });
test('returns true for own instance', () => { test('returns true for own instance', () => {
expect(checkCanInvite('wrld_123:instance~private(usr_me)', defaultInviteDeps)).toBe( expect(
true checkCanInvite(
); 'wrld_123:instance~private(usr_me)',
defaultInviteDeps
)
).toBe(true);
}); });
test('returns false for invite-only instance owned by another', () => { test('returns false for invite-only instance owned by another', () => {
expect(checkCanInvite('wrld_123:instance~private(usr_other)', defaultInviteDeps)).toBe( expect(
false checkCanInvite(
); 'wrld_123:instance~private(usr_other)',
defaultInviteDeps
)
).toBe(false);
}); });
test('returns false for friends-only instance', () => { test('returns false for friends-only instance', () => {
expect(checkCanInvite('wrld_123:instance~friends(usr_other)', defaultInviteDeps)).toBe( expect(
false checkCanInvite(
); 'wrld_123:instance~friends(usr_other)',
defaultInviteDeps
)
).toBe(false);
}); });
test('returns true for friends+ instance if current location matches', () => { test('returns true for friends+ instance if current location matches', () => {
const location = 'wrld_123:instance~hidden(usr_other)'; const location = 'wrld_123:instance~hidden(usr_other)';
expect(checkCanInvite(location, { expect(
...defaultInviteDeps, checkCanInvite(location, {
lastLocationStr: location ...defaultInviteDeps,
})).toBe(true); lastLocationStr: location
})
).toBe(true);
}); });
test('returns false for friends+ instance if not in that location', () => { test('returns false for friends+ instance if not in that location', () => {
expect(checkCanInvite('wrld_123:instance~hidden(usr_other)', defaultInviteDeps)).toBe( expect(
false checkCanInvite(
); 'wrld_123:instance~hidden(usr_other)',
defaultInviteDeps
)
).toBe(false);
}); });
test('returns false for closed instance', () => { test('returns false for closed instance', () => {
const location = 'wrld_123:instance'; const location = 'wrld_123:instance';
expect(checkCanInvite(location, { expect(
...defaultInviteDeps, checkCanInvite(location, {
cachedInstances: new Map([ ...defaultInviteDeps,
[location, { closedAt: '2024-01-01' }] cachedInstances: new Map([
]) [location, { closedAt: '2024-01-01' }]
})).toBe(false); ])
})
).toBe(false);
}); });
}); });
@@ -113,12 +131,17 @@ describe('Invite Utils', () => {
test('returns true for own instance', () => { test('returns true for own instance', () => {
expect( expect(
checkCanInviteSelf('wrld_123:instance~private(usr_me)', defaultSelfDeps) checkCanInviteSelf(
'wrld_123:instance~private(usr_me)',
defaultSelfDeps
)
).toBe(true); ).toBe(true);
}); });
test('returns true for public instance', () => { test('returns true for public instance', () => {
expect(checkCanInviteSelf('wrld_123:instance', defaultSelfDeps)).toBe(true); expect(
checkCanInviteSelf('wrld_123:instance', defaultSelfDeps)
).toBe(true);
}); });
test('returns true for friends-only instance if user is a friend', () => { test('returns true for friends-only instance if user is a friend', () => {
@@ -132,23 +155,31 @@ describe('Invite Utils', () => {
test('returns false for friends-only instance if user is not a friend', () => { test('returns false for friends-only instance if user is not a friend', () => {
expect( expect(
checkCanInviteSelf('wrld_123:instance~friends(usr_other)', defaultSelfDeps) checkCanInviteSelf(
'wrld_123:instance~friends(usr_other)',
defaultSelfDeps
)
).toBe(false); ).toBe(false);
}); });
test('returns false for closed instance', () => { test('returns false for closed instance', () => {
const location = 'wrld_123:instance'; const location = 'wrld_123:instance';
expect(checkCanInviteSelf(location, { expect(
...defaultSelfDeps, checkCanInviteSelf(location, {
cachedInstances: new Map([ ...defaultSelfDeps,
[location, { closedAt: '2024-01-01' }] cachedInstances: new Map([
]) [location, { closedAt: '2024-01-01' }]
})).toBe(false); ])
})
).toBe(false);
}); });
test('returns true for invite instance (not owned, not closed)', () => { test('returns true for invite instance (not owned, not closed)', () => {
expect( expect(
checkCanInviteSelf('wrld_123:instance~private(usr_other)', defaultSelfDeps) checkCanInviteSelf(
'wrld_123:instance~private(usr_other)',
defaultSelfDeps
)
).toBe(true); ).toBe(true);
}); });
}); });
+93 -57
View File
@@ -319,68 +319,92 @@ describe('User Utils', () => {
} }
]; ];
for (const { status, location, state, expected } of cases) { for (const { status, location, state, expected } of cases) {
const result = userStatusClass({ const result = userStatusClass(
id: 'usr_friend', {
isFriend: true, id: 'usr_friend',
status, isFriend: true,
location, status,
state location,
}, false, currentUser); state
},
false,
currentUser
);
expect(result[expected]).toBe(true); expect(result[expected]).toBe(true);
} }
}); });
test('returns offline style for location offline', () => { test('returns offline style for location offline', () => {
const result = userStatusClass({ const result = userStatusClass(
id: 'usr_f', {
isFriend: true, id: 'usr_f',
status: 'active', isFriend: true,
location: 'offline', status: 'active',
state: '' location: 'offline',
}, false, currentUser); state: ''
},
false,
currentUser
);
expect(result.offline).toBe(true); expect(result.offline).toBe(true);
}); });
test('returns active style for state active', () => { test('returns active style for state active', () => {
const result = userStatusClass({ const result = userStatusClass(
id: 'usr_f', {
isFriend: true, id: 'usr_f',
status: 'busy', isFriend: true,
location: 'private', status: 'busy',
state: 'active' location: 'private',
}, false, currentUser); state: 'active'
},
false,
currentUser
);
expect(result.active).toBe(true); expect(result.active).toBe(true);
}); });
test('sets mobile flag for non-PC platform friend', () => { test('sets mobile flag for non-PC platform friend', () => {
const result = userStatusClass({ const result = userStatusClass(
id: 'usr_f', {
isFriend: true, id: 'usr_f',
status: 'active', isFriend: true,
location: 'wrld_1', status: 'active',
state: 'online', location: 'wrld_1',
$platform: 'android' state: 'online',
}, false, currentUser); $platform: 'android'
},
false,
currentUser
);
expect(result.mobile).toBe(true); expect(result.mobile).toBe(true);
}); });
test('no mobile flag for standalonewindows platform', () => { test('no mobile flag for standalonewindows platform', () => {
const result = userStatusClass({ const result = userStatusClass(
id: 'usr_f', {
isFriend: true, id: 'usr_f',
status: 'active', isFriend: true,
location: 'wrld_1', status: 'active',
state: 'online', location: 'wrld_1',
$platform: 'standalonewindows' state: 'online',
}, false, currentUser); $platform: 'standalonewindows'
},
false,
currentUser
);
expect(result.mobile).toBeUndefined(); expect(result.mobile).toBeUndefined();
}); });
test('uses userId as fallback when id is not present', () => { test('uses userId as fallback when id is not present', () => {
const result = userStatusClass({ const result = userStatusClass(
userId: 'usr_me', {
status: 'busy' userId: 'usr_me',
}, false, currentUser); status: 'busy'
},
false,
currentUser
);
expect(result).toMatchObject({ expect(result).toMatchObject({
'status-icon': true, 'status-icon': true,
busy: true, busy: true,
@@ -390,26 +414,34 @@ describe('User Utils', () => {
test('handles private location with empty state (temp fix branch)', () => { test('handles private location with empty state (temp fix branch)', () => {
currentUser.activeFriends = ['usr_f']; currentUser.activeFriends = ['usr_f'];
const result = userStatusClass({ const result = userStatusClass(
id: 'usr_f', {
isFriend: true, id: 'usr_f',
status: 'busy', isFriend: true,
location: 'private', status: 'busy',
state: '' location: 'private',
}, false, currentUser); state: ''
},
false,
currentUser
);
// activeFriends includes usr_f → active // activeFriends includes usr_f → active
expect(result.active).toBe(true); expect(result.active).toBe(true);
}); });
test('handles private location temp fix → offline branch', () => { test('handles private location temp fix → offline branch', () => {
currentUser.activeFriends = []; currentUser.activeFriends = [];
const result = userStatusClass({ const result = userStatusClass(
id: 'usr_f', {
isFriend: true, id: 'usr_f',
status: 'busy', isFriend: true,
location: 'private', status: 'busy',
state: '' location: 'private',
}, false, currentUser); state: ''
},
false,
currentUser
);
expect(result.offline).toBe(true); expect(result.offline).toBe(true);
}); });
}); });
@@ -423,7 +455,9 @@ describe('User Utils', () => {
false, false,
false false
); );
expect(storeMocks.useAppearanceSettingsStore).not.toHaveBeenCalled(); expect(
storeMocks.useAppearanceSettingsStore
).not.toHaveBeenCalled();
}); });
test('returns empty string for falsy user', () => { test('returns empty string for falsy user', () => {
@@ -539,7 +573,9 @@ describe('User Utils', () => {
{ currentAvatarImageUrl: 'https://img.com/avatar' }, { currentAvatarImageUrl: 'https://img.com/avatar' },
false false
); );
expect(storeMocks.useAppearanceSettingsStore).not.toHaveBeenCalled(); expect(
storeMocks.useAppearanceSettingsStore
).not.toHaveBeenCalled();
}); });
test('returns empty string for falsy user', () => { test('returns empty string for falsy user', () => {
+16 -5
View File
@@ -50,10 +50,18 @@ const showAvatarDialog = vi.fn();
const showWorldDialog = vi.fn(); const showWorldDialog = vi.fn();
const showGroupDialog = vi.fn(); const showGroupDialog = vi.fn();
vi.mock('../../coordinators/userCoordinator', () => ({ showUserDialog: (...args) => showUserDialog(...args) })); vi.mock('../../coordinators/userCoordinator', () => ({
vi.mock('../../coordinators/avatarCoordinator', () => ({ showAvatarDialog: (...args) => showAvatarDialog(...args) })); showUserDialog: (...args) => showUserDialog(...args)
vi.mock('../../coordinators/worldCoordinator', () => ({ showWorldDialog: (...args) => showWorldDialog(...args) })); }));
vi.mock('../../coordinators/groupCoordinator', () => ({ showGroupDialog: (...args) => showGroupDialog(...args) })); vi.mock('../../coordinators/avatarCoordinator', () => ({
showAvatarDialog: (...args) => showAvatarDialog(...args)
}));
vi.mock('../../coordinators/worldCoordinator', () => ({
showWorldDialog: (...args) => showWorldDialog(...args)
}));
vi.mock('../../coordinators/groupCoordinator', () => ({
showGroupDialog: (...args) => showGroupDialog(...args)
}));
import { useGlobalSearchStore } from '../globalSearch'; import { useGlobalSearchStore } from '../globalSearch';
@@ -104,7 +112,10 @@ describe('useGlobalSearchStore', () => {
}); });
expect(store.friendResults).toEqual([]); expect(store.friendResults).toEqual([]);
mocks.friendStore.friends.set('usr_new', { id: 'usr_new', ref: { id: 'usr_new' } }); mocks.friendStore.friends.set('usr_new', {
id: 'usr_new',
ref: { id: 'usr_new' }
});
worker.emit({ worker.emit({
type: 'searchResult', type: 'searchResult',
payload: { payload: {
+1 -3
View File
@@ -119,9 +119,7 @@ describe('notyGetImage', () => {
test('returns empty string when user lookup fails', async () => { test('returns empty string when user lookup fails', async () => {
deps.getUserIdFromNoty.mockReturnValue('usr_abc'); deps.getUserIdFromNoty.mockReturnValue('usr_abc');
deps.queryRequest.fetch.mockRejectedValue( deps.queryRequest.fetch.mockRejectedValue(new Error('Network error'));
new Error('Network error')
);
dispatch = createOverlayDispatch(deps); dispatch = createOverlayDispatch(deps);
const result = await dispatch.notyGetImage({}); const result = await dispatch.notyGetImage({});
+4 -6
View File
@@ -221,9 +221,7 @@ describe('useSearchStore', () => {
}); });
test('parses vrchat.com group URL', () => { test('parses vrchat.com group URL', () => {
store.directAccessParse( store.directAccessParse('https://vrchat.com/home/group/grp_abc123');
'https://vrchat.com/home/group/grp_abc123'
);
expect(mockShowGroupDialog).toHaveBeenCalledWith('grp_abc123'); expect(mockShowGroupDialog).toHaveBeenCalledWith('grp_abc123');
}); });
@@ -248,9 +246,9 @@ describe('useSearchStore', () => {
}); });
test('returns false for short vrchat URL with insufficient path segments', () => { test('returns false for short vrchat URL with insufficient path segments', () => {
expect( expect(store.directAccessParse('https://vrchat.com/home')).toBe(
store.directAccessParse('https://vrchat.com/home') false
).toBe(false); );
}); });
}); });
+39 -6
View File
@@ -30,7 +30,12 @@ describe('searchWorker message protocol', () => {
harness.dispatch({ harness.dispatch({
type: 'search', type: 'search',
payload: { seq: 7, query: 'a', currentUserId: 'usr_me', language: 'en-US' } payload: {
seq: 7,
query: 'a',
currentUserId: 'usr_me',
language: 'en-US'
}
}); });
expect(harness.sent).toHaveLength(1); expect(harness.sent).toHaveLength(1);
@@ -57,17 +62,45 @@ describe('searchWorker message protocol', () => {
type: 'updateIndex', type: 'updateIndex',
payload: { payload: {
friends: [], friends: [],
avatars: [{ id: 'avtr_1', name: 'Alpha Avatar', authorId: 'usr_me', imageUrl: '' }], avatars: [
worlds: [{ id: 'wrld_1', name: 'Alpha World', authorId: 'usr_me', imageUrl: '' }], {
groups: [{ id: 'grp_1', name: 'Alpha Group', ownerId: 'usr_me', imageUrl: '' }], id: 'avtr_1',
favAvatars: [{ id: 'avtr_1', name: 'Alpha Avatar', imageUrl: '' }], name: 'Alpha Avatar',
authorId: 'usr_me',
imageUrl: ''
}
],
worlds: [
{
id: 'wrld_1',
name: 'Alpha World',
authorId: 'usr_me',
imageUrl: ''
}
],
groups: [
{
id: 'grp_1',
name: 'Alpha Group',
ownerId: 'usr_me',
imageUrl: ''
}
],
favAvatars: [
{ id: 'avtr_1', name: 'Alpha Avatar', imageUrl: '' }
],
favWorlds: [{ id: 'wrld_1', name: 'Alpha World', imageUrl: '' }] favWorlds: [{ id: 'wrld_1', name: 'Alpha World', imageUrl: '' }]
} }
}); });
harness.dispatch({ harness.dispatch({
type: 'search', type: 'search',
payload: { seq: 8, query: 'Alpha', currentUserId: 'usr_me', language: 'en-US' } payload: {
seq: 8,
query: 'Alpha',
currentUserId: 'usr_me',
language: 'en-US'
}
}); });
const result = harness.sent.at(-1); const result = harness.sent.at(-1);
+3 -4
View File
@@ -28,10 +28,9 @@ vi.mock('vue-sonner', () => ({
vi.mock('vue-i18n', () => ({ vi.mock('vue-i18n', () => ({
useI18n: () => ({ useI18n: () => ({
t: (key) => key t: (key) => key,
, locale: require('vue').ref('en')
locale: require('vue').ref('en') })
})
})); }));
function flushPromises() { function flushPromises() {
+4 -2
View File
@@ -3,7 +3,10 @@ import { defineStore } from 'pinia';
import { checkVRChatCache } from '../shared/utils'; import { checkVRChatCache } from '../shared/utils';
import { queryRequest } from '../api'; import { queryRequest } from '../api';
import { getAvatarHistory, preloadOwnAvatars } from '../coordinators/avatarCoordinator'; import {
getAvatarHistory,
preloadOwnAvatars
} from '../coordinators/avatarCoordinator';
import { database } from '../services/database'; import { database } from '../services/database';
import { watchState } from '../services/watchState'; import { watchState } from '../services/watchState';
@@ -84,7 +87,6 @@ export const useAvatarStore = defineStore('Avatar', () => {
* @returns {Promise<string[]>} * @returns {Promise<string[]>}
*/ */
async function getAvatarGallery(avatarId) { async function getAvatarGallery(avatarId) {
const D = avatarDialog.value; const D = avatarDialog.value;
const args = await queryRequest const args = await queryRequest
.fetch('avatarGallery', { avatarId }) .fetch('avatarGallery', { avatarId })
+11 -7
View File
@@ -12,12 +12,18 @@ function clonePanel(panel) {
if (typeof panel === 'string' && panel) { if (typeof panel === 'string' && panel) {
return panel; return panel;
} }
if (panel && typeof panel === 'object' && typeof panel.key === 'string' && panel.key) { if (
panel &&
typeof panel === 'object' &&
typeof panel.key === 'string' &&
panel.key
) {
return { return {
key: panel.key, key: panel.key,
config: panel.config && typeof panel.config === 'object' config:
? JSON.parse(JSON.stringify(panel.config)) panel.config && typeof panel.config === 'object'
: {} ? JSON.parse(JSON.stringify(panel.config))
: {}
}; };
} }
return null; return null;
@@ -139,9 +145,7 @@ export const useDashboardStore = defineStore('dashboard', () => {
} }
function generateNextDashboardName(baseName = 'Dashboard') { function generateNextDashboardName(baseName = 'Dashboard') {
const existingNames = new Set( const existingNames = new Set(dashboards.value.map((d) => d.name));
dashboards.value.map((d) => d.name)
);
if (!existingNames.has(baseName)) { if (!existingNames.has(baseName)) {
return baseName; return baseName;
} }
-1
View File
@@ -241,7 +241,6 @@ export const useGlobalSearchStore = defineStore('GlobalSearch', () => {
joinedGroupResults.value = []; joinedGroupResults.value = [];
} }
function open() { function open() {
sendIndexUpdate(); sendIndexUpdate();
isOpen.value = true; isOpen.value = true;
+2 -1
View File
@@ -258,7 +258,8 @@ export const useInstanceStore = defineStore('Instance', () => {
emptyDefault: { id: '', name: '' }, emptyDefault: { id: '', name: '' },
idAlias: 'worldId', idAlias: 'worldId',
nameKey: 'name', nameKey: 'name',
fetchFn: (id) => queryRequest.fetch('world.location', { worldId: id }) fetchFn: (id) =>
queryRequest.fetch('world.location', { worldId: id })
}); });
} }
+69 -17
View File
@@ -124,12 +124,12 @@ function isPrefixMatch(name, query, comparer) {
// ── Index data (updated from main thread) ─────────────────────────── // ── Index data (updated from main thread) ───────────────────────────
let indexedFriends = []; // { id, name, memo, note, imageUrl } let indexedFriends = []; // { id, name, memo, note, imageUrl }
let indexedAvatars = []; // { id, name, authorId, imageUrl } let indexedAvatars = []; // { id, name, authorId, imageUrl }
let indexedWorlds = []; // { id, name, authorId, imageUrl } let indexedWorlds = []; // { id, name, authorId, imageUrl }
let indexedGroups = []; // { id, name, ownerId, imageUrl } let indexedGroups = []; // { id, name, ownerId, imageUrl }
let indexedFavAvatars = []; // { id, name, imageUrl } let indexedFavAvatars = []; // { id, name, imageUrl }
let indexedFavWorlds = []; // { id, name, imageUrl } let indexedFavWorlds = []; // { id, name, imageUrl }
/** /**
* Update the search index with fresh data snapshots. * Update the search index with fresh data snapshots.
@@ -181,7 +181,15 @@ function searchFriends(query, comparer, limit = 10) {
return results; return results;
} }
function searchItems(query, items, type, comparer, ownerKey, ownerId, limit = 10) { function searchItems(
query,
items,
type,
comparer,
ownerKey,
ownerId,
limit = 10
) {
const results = []; const results = [];
for (const ref of items) { for (const ref of items) {
if (!ref || !ref.name) continue; if (!ref || !ref.name) continue;
@@ -226,18 +234,60 @@ function handleSearch(payload) {
return; return;
} }
const comparer = new Intl.Collator( const comparer = new Intl.Collator((language || 'en').replace('_', '-'), {
(language || 'en').replace('_', '-'), usage: 'search',
{ usage: 'search', sensitivity: 'base' } sensitivity: 'base'
); });
const friends = searchFriends(query, comparer); const friends = searchFriends(query, comparer);
const ownAvatars = searchItems(query, indexedAvatars, 'avatar', comparer, 'authorId', currentUserId); const ownAvatars = searchItems(
const favAvatars = searchItems(query, indexedFavAvatars, 'avatar', comparer, null, null); query,
const ownWorlds = searchItems(query, indexedWorlds, 'world', comparer, 'authorId', currentUserId); indexedAvatars,
const favWorlds = searchItems(query, indexedFavWorlds, 'world', comparer, null, null); 'avatar',
const ownGroups = searchItems(query, indexedGroups, 'group', comparer, 'ownerId', currentUserId); comparer,
const joinedGroups = searchItems(query, indexedGroups, 'group', comparer, null, null); 'authorId',
currentUserId
);
const favAvatars = searchItems(
query,
indexedFavAvatars,
'avatar',
comparer,
null,
null
);
const ownWorlds = searchItems(
query,
indexedWorlds,
'world',
comparer,
'authorId',
currentUserId
);
const favWorlds = searchItems(
query,
indexedFavWorlds,
'world',
comparer,
null,
null
);
const ownGroups = searchItems(
query,
indexedGroups,
'group',
comparer,
'ownerId',
currentUserId
);
const joinedGroups = searchItems(
query,
indexedGroups,
'group',
comparer,
null,
null
);
// Deduplicate favorites against own // Deduplicate favorites against own
const ownAvatarIds = new Set(ownAvatars.map((r) => r.id)); const ownAvatarIds = new Set(ownAvatars.map((r) => r.id));
@@ -245,7 +295,9 @@ function handleSearch(payload) {
const ownWorldIds = new Set(ownWorlds.map((r) => r.id)); const ownWorldIds = new Set(ownWorlds.map((r) => r.id));
const dedupedFavWorlds = favWorlds.filter((r) => !ownWorldIds.has(r.id)); const dedupedFavWorlds = favWorlds.filter((r) => !ownWorldIds.has(r.id));
const ownGroupIds = new Set(ownGroups.map((r) => r.id)); const ownGroupIds = new Set(ownGroups.map((r) => r.id));
const dedupedJoinedGroups = joinedGroups.filter((r) => !ownGroupIds.has(r.id)); const dedupedJoinedGroups = joinedGroups.filter(
(r) => !ownGroupIds.has(r.id)
);
self.postMessage({ self.postMessage({
type: 'searchResult', type: 'searchResult',
+63 -63
View File
@@ -1,68 +1,68 @@
:root[data-theme='light'] { :root[data-theme='light'] {
--radius: 0.65rem; --radius: 0.65rem;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823); --foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823); --card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823); --popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.488 0.243 264.376); --primary: oklch(0.488 0.243 264.376);
--primary-foreground: oklch(0.97 0.014 254.604); --primary-foreground: oklch(0.97 0.014 254.604);
--secondary: oklch(0.967 0.001 286.375); --secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885); --secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375); --muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938); --muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375); --accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885); --accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32); --border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32); --input: oklch(0.92 0.004 286.32);
--ring: oklch(0.708 0 0); --ring: oklch(0.708 0 0);
--chart-1: oklch(0.809 0.105 251.813); --chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815); --chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881); --chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376); --chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638); --chart-5: oklch(0.424 0.199 265.638);
--sidebar: oklch(0.985 0 0); --sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823); --sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.546 0.245 262.881); --sidebar-primary: oklch(0.546 0.245 262.881);
--sidebar-primary-foreground: oklch(0.97 0.014 254.604); --sidebar-primary-foreground: oklch(0.97 0.014 254.604);
--sidebar-accent: oklch(0.967 0.001 286.375); --sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885); --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32); --sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: oklch(0.708 0 0);
} }
:root[data-theme='dark'] { :root[data-theme='dark'] {
--background: oklch(0.141 0.005 285.823); --background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885); --card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885); --popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.488 0.243 264.376); --primary: oklch(0.488 0.243 264.376);
--primary-foreground: oklch(0.97 0.014 254.604); --primary-foreground: oklch(0.97 0.014 254.604);
--secondary: oklch(0.274 0.006 286.033); --secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033); --muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067); --muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033); --accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%); --border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%); --input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0); --ring: oklch(0.556 0 0);
--chart-1: oklch(0.809 0.105 251.813); --chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815); --chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881); --chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376); --chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638); --chart-5: oklch(0.424 0.199 265.638);
--sidebar: oklch(0.21 0.006 285.885); --sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.623 0.214 259.815); --sidebar-primary: oklch(0.623 0.214 259.815);
--sidebar-primary-foreground: oklch(0.97 0.014 254.604); --sidebar-primary-foreground: oklch(0.97 0.014 254.604);
--sidebar-accent: oklch(0.274 0.006 286.033); --sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.439 0 0); --sidebar-ring: oklch(0.439 0 0);
} }
+63 -63
View File
@@ -1,68 +1,68 @@
:root[data-theme='light'] { :root[data-theme='light'] {
--radius: 0.65rem; --radius: 0.65rem;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823); --foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823); --card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823); --popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.648 0.2 131.684); --primary: oklch(0.648 0.2 131.684);
--primary-foreground: oklch(0.986 0.031 120.757); --primary-foreground: oklch(0.986 0.031 120.757);
--secondary: oklch(0.967 0.001 286.375); --secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885); --secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375); --muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938); --muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375); --accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885); --accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32); --border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32); --input: oklch(0.92 0.004 286.32);
--ring: oklch(0.841 0.238 128.85); --ring: oklch(0.841 0.238 128.85);
--chart-1: oklch(0.871 0.15 154.449); --chart-1: oklch(0.871 0.15 154.449);
--chart-2: oklch(0.723 0.219 149.579); --chart-2: oklch(0.723 0.219 149.579);
--chart-3: oklch(0.627 0.194 149.214); --chart-3: oklch(0.627 0.194 149.214);
--chart-4: oklch(0.527 0.154 150.069); --chart-4: oklch(0.527 0.154 150.069);
--chart-5: oklch(0.448 0.119 151.328); --chart-5: oklch(0.448 0.119 151.328);
--sidebar: oklch(0.985 0 0); --sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823); --sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.648 0.2 131.684); --sidebar-primary: oklch(0.648 0.2 131.684);
--sidebar-primary-foreground: oklch(0.986 0.031 120.757); --sidebar-primary-foreground: oklch(0.986 0.031 120.757);
--sidebar-accent: oklch(0.967 0.001 286.375); --sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885); --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32); --sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.841 0.238 128.85); --sidebar-ring: oklch(0.841 0.238 128.85);
} }
:root[data-theme='dark'] { :root[data-theme='dark'] {
--background: oklch(0.141 0.005 285.823); --background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885); --card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885); --popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.648 0.2 131.684); --primary: oklch(0.648 0.2 131.684);
--primary-foreground: oklch(0.986 0.031 120.757); --primary-foreground: oklch(0.986 0.031 120.757);
--secondary: oklch(0.274 0.006 286.033); --secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033); --muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067); --muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033); --accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%); --border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%); --input: oklch(1 0 0 / 15%);
--ring: oklch(0.405 0.101 131.063); --ring: oklch(0.405 0.101 131.063);
--chart-1: oklch(0.871 0.15 154.449); --chart-1: oklch(0.871 0.15 154.449);
--chart-2: oklch(0.723 0.219 149.579); --chart-2: oklch(0.723 0.219 149.579);
--chart-3: oklch(0.627 0.194 149.214); --chart-3: oklch(0.627 0.194 149.214);
--chart-4: oklch(0.527 0.154 150.069); --chart-4: oklch(0.527 0.154 150.069);
--chart-5: oklch(0.448 0.119 151.328); --chart-5: oklch(0.448 0.119 151.328);
--sidebar: oklch(0.21 0.006 285.885); --sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.768 0.233 130.85); --sidebar-primary: oklch(0.768 0.233 130.85);
--sidebar-primary-foreground: oklch(0.986 0.031 120.757); --sidebar-primary-foreground: oklch(0.986 0.031 120.757);
--sidebar-accent: oklch(0.274 0.006 286.033); --sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.405 0.101 131.063); --sidebar-ring: oklch(0.405 0.101 131.063);
} }
+35 -35
View File
@@ -36,63 +36,63 @@
/* blue */ /* blue */
:root[data-theme='midnight'][data-theme-color='blue'] { :root[data-theme='midnight'][data-theme-color='blue'] {
--primary: oklch(0.488 0.243 264.376); --primary: oklch(0.488 0.243 264.376);
--primary-foreground: oklch(0.97 0.014 254.604); --primary-foreground: oklch(0.97 0.014 254.604);
--accent: oklch(0.274 0.006 286.033); --accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--ring: oklch(0.556 0 0); --ring: oklch(0.556 0 0);
} }
/* green */ /* green */
:root[data-theme='midnight'][data-theme-color='green'] { :root[data-theme='midnight'][data-theme-color='green'] {
--primary: oklch(0.648 0.2 131.684); --primary: oklch(0.648 0.2 131.684);
--primary-foreground: oklch(0.986 0.031 120.757); --primary-foreground: oklch(0.986 0.031 120.757);
--accent: oklch(0.274 0.006 286.033); --accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--ring: oklch(0.405 0.101 131.063); --ring: oklch(0.405 0.101 131.063);
} }
/* orange */ /* orange */
:root[data-theme='midnight'][data-theme-color='orange'] { :root[data-theme='midnight'][data-theme-color='orange'] {
--primary: oklch(0.705 0.213 47.604); --primary: oklch(0.705 0.213 47.604);
--primary-foreground: oklch(0.98 0.016 73.684); --primary-foreground: oklch(0.98 0.016 73.684);
--accent: oklch(0.274 0.006 286.033); --accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--ring: oklch(0.408 0.123 38.172); --ring: oklch(0.408 0.123 38.172);
} }
/* red */ /* red */
:root[data-theme='midnight'][data-theme-color='red'] { :root[data-theme='midnight'][data-theme-color='red'] {
--primary: oklch(0.637 0.237 25.331); --primary: oklch(0.637 0.237 25.331);
--primary-foreground: oklch(0.971 0.013 17.38); --primary-foreground: oklch(0.971 0.013 17.38);
--accent: oklch(0.274 0.006 286.033); --accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--ring: oklch(0.396 0.141 25.723); --ring: oklch(0.396 0.141 25.723);
} }
/* rose */ /* rose */
:root[data-theme='midnight'][data-theme-color='rose'] { :root[data-theme='midnight'][data-theme-color='rose'] {
--primary: oklch(0.645 0.246 16.439); --primary: oklch(0.645 0.246 16.439);
--primary-foreground: oklch(0.969 0.015 12.422); --primary-foreground: oklch(0.969 0.015 12.422);
--accent: oklch(0.274 0.006 286.033); --accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--ring: oklch(0.41 0.159 10.272); --ring: oklch(0.41 0.159 10.272);
} }
/* violet */ /* violet */
:root[data-theme='midnight'][data-theme-color='violet'] { :root[data-theme='midnight'][data-theme-color='violet'] {
--primary: oklch(0.606 0.25 292.717); --primary: oklch(0.606 0.25 292.717);
--primary-foreground: oklch(0.969 0.016 293.756); --primary-foreground: oklch(0.969 0.016 293.756);
--accent: oklch(0.274 0.006 286.033); --accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--ring: oklch(0.38 0.189 293.745); --ring: oklch(0.38 0.189 293.745);
} }
/* yellow */ /* yellow */
:root[data-theme='midnight'][data-theme-color='yellow'] { :root[data-theme='midnight'][data-theme-color='yellow'] {
--primary: oklch(0.795 0.184 86.047); --primary: oklch(0.795 0.184 86.047);
--primary-foreground: oklch(0.421 0.095 57.708); --primary-foreground: oklch(0.421 0.095 57.708);
--accent: oklch(0.274 0.006 286.033); --accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--ring: oklch(0.421 0.095 57.708); --ring: oklch(0.421 0.095 57.708);
} }
+63 -63
View File
@@ -1,68 +1,68 @@
:root[data-theme='light'] { :root[data-theme='light'] {
--radius: 0.65rem; --radius: 0.65rem;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823); --foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823); --card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823); --popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.646 0.222 41.116); --primary: oklch(0.646 0.222 41.116);
--primary-foreground: oklch(0.98 0.016 73.684); --primary-foreground: oklch(0.98 0.016 73.684);
--secondary: oklch(0.967 0.001 286.375); --secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885); --secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375); --muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938); --muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375); --accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885); --accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32); --border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32); --input: oklch(0.92 0.004 286.32);
--ring: oklch(0.75 0.183 55.934); --ring: oklch(0.75 0.183 55.934);
--chart-1: oklch(0.837 0.128 66.29); --chart-1: oklch(0.837 0.128 66.29);
--chart-2: oklch(0.705 0.213 47.604); --chart-2: oklch(0.705 0.213 47.604);
--chart-3: oklch(0.646 0.222 41.116); --chart-3: oklch(0.646 0.222 41.116);
--chart-4: oklch(0.553 0.195 38.402); --chart-4: oklch(0.553 0.195 38.402);
--chart-5: oklch(0.47 0.157 37.304); --chart-5: oklch(0.47 0.157 37.304);
--sidebar: oklch(0.985 0 0); --sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823); --sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.646 0.222 41.116); --sidebar-primary: oklch(0.646 0.222 41.116);
--sidebar-primary-foreground: oklch(0.98 0.016 73.684); --sidebar-primary-foreground: oklch(0.98 0.016 73.684);
--sidebar-accent: oklch(0.967 0.001 286.375); --sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885); --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32); --sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.75 0.183 55.934); --sidebar-ring: oklch(0.75 0.183 55.934);
} }
:root[data-theme='dark'] { :root[data-theme='dark'] {
--background: oklch(0.141 0.005 285.823); --background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885); --card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885); --popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.705 0.213 47.604); --primary: oklch(0.705 0.213 47.604);
--primary-foreground: oklch(0.98 0.016 73.684); --primary-foreground: oklch(0.98 0.016 73.684);
--secondary: oklch(0.274 0.006 286.033); --secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033); --muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067); --muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033); --accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%); --border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%); --input: oklch(1 0 0 / 15%);
--ring: oklch(0.408 0.123 38.172); --ring: oklch(0.408 0.123 38.172);
--chart-1: oklch(0.837 0.128 66.29); --chart-1: oklch(0.837 0.128 66.29);
--chart-2: oklch(0.705 0.213 47.604); --chart-2: oklch(0.705 0.213 47.604);
--chart-3: oklch(0.646 0.222 41.116); --chart-3: oklch(0.646 0.222 41.116);
--chart-4: oklch(0.553 0.195 38.402); --chart-4: oklch(0.553 0.195 38.402);
--chart-5: oklch(0.47 0.157 37.304); --chart-5: oklch(0.47 0.157 37.304);
--sidebar: oklch(0.21 0.006 285.885); --sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.705 0.213 47.604); --sidebar-primary: oklch(0.705 0.213 47.604);
--sidebar-primary-foreground: oklch(0.98 0.016 73.684); --sidebar-primary-foreground: oklch(0.98 0.016 73.684);
--sidebar-accent: oklch(0.274 0.006 286.033); --sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.408 0.123 38.172); --sidebar-ring: oklch(0.408 0.123 38.172);
} }
+63 -63
View File
@@ -1,68 +1,68 @@
:root[data-theme='light'] { :root[data-theme='light'] {
--radius: 0.65rem; --radius: 0.65rem;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823); --foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823); --card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823); --popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.577 0.245 27.325); --primary: oklch(0.577 0.245 27.325);
--primary-foreground: oklch(0.971 0.013 17.38); --primary-foreground: oklch(0.971 0.013 17.38);
--secondary: oklch(0.967 0.001 286.375); --secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885); --secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375); --muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938); --muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375); --accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885); --accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32); --border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32); --input: oklch(0.92 0.004 286.32);
--ring: oklch(0.704 0.191 22.216); --ring: oklch(0.704 0.191 22.216);
--chart-1: oklch(0.808 0.114 19.571); --chart-1: oklch(0.808 0.114 19.571);
--chart-2: oklch(0.637 0.237 25.331); --chart-2: oklch(0.637 0.237 25.331);
--chart-3: oklch(0.577 0.245 27.325); --chart-3: oklch(0.577 0.245 27.325);
--chart-4: oklch(0.505 0.213 27.518); --chart-4: oklch(0.505 0.213 27.518);
--chart-5: oklch(0.444 0.177 26.899); --chart-5: oklch(0.444 0.177 26.899);
--sidebar: oklch(0.985 0 0); --sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823); --sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.577 0.245 27.325); --sidebar-primary: oklch(0.577 0.245 27.325);
--sidebar-primary-foreground: oklch(0.971 0.013 17.38); --sidebar-primary-foreground: oklch(0.971 0.013 17.38);
--sidebar-accent: oklch(0.967 0.001 286.375); --sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885); --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32); --sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.704 0.191 22.216); --sidebar-ring: oklch(0.704 0.191 22.216);
} }
:root[data-theme='dark'] { :root[data-theme='dark'] {
--background: oklch(0.141 0.005 285.823); --background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885); --card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885); --popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.637 0.237 25.331); --primary: oklch(0.637 0.237 25.331);
--primary-foreground: oklch(0.971 0.013 17.38); --primary-foreground: oklch(0.971 0.013 17.38);
--secondary: oklch(0.274 0.006 286.033); --secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033); --muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067); --muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033); --accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%); --border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%); --input: oklch(1 0 0 / 15%);
--ring: oklch(0.396 0.141 25.723); --ring: oklch(0.396 0.141 25.723);
--chart-1: oklch(0.808 0.114 19.571); --chart-1: oklch(0.808 0.114 19.571);
--chart-2: oklch(0.637 0.237 25.331); --chart-2: oklch(0.637 0.237 25.331);
--chart-3: oklch(0.577 0.245 27.325); --chart-3: oklch(0.577 0.245 27.325);
--chart-4: oklch(0.505 0.213 27.518); --chart-4: oklch(0.505 0.213 27.518);
--chart-5: oklch(0.444 0.177 26.899); --chart-5: oklch(0.444 0.177 26.899);
--sidebar: oklch(0.21 0.006 285.885); --sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.637 0.237 25.331); --sidebar-primary: oklch(0.637 0.237 25.331);
--sidebar-primary-foreground: oklch(0.971 0.013 17.38); --sidebar-primary-foreground: oklch(0.971 0.013 17.38);
--sidebar-accent: oklch(0.274 0.006 286.033); --sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.396 0.141 25.723); --sidebar-ring: oklch(0.396 0.141 25.723);
} }
+63 -63
View File
@@ -1,68 +1,68 @@
:root[data-theme='light'] { :root[data-theme='light'] {
--radius: 0.65rem; --radius: 0.65rem;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823); --foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823); --card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823); --popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.586 0.253 17.585); --primary: oklch(0.586 0.253 17.585);
--primary-foreground: oklch(0.969 0.015 12.422); --primary-foreground: oklch(0.969 0.015 12.422);
--secondary: oklch(0.967 0.001 286.375); --secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885); --secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375); --muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938); --muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375); --accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885); --accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32); --border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32); --input: oklch(0.92 0.004 286.32);
--ring: oklch(0.712 0.194 13.428); --ring: oklch(0.712 0.194 13.428);
--chart-1: oklch(0.81 0.117 11.638); --chart-1: oklch(0.81 0.117 11.638);
--chart-2: oklch(0.645 0.246 16.439); --chart-2: oklch(0.645 0.246 16.439);
--chart-3: oklch(0.586 0.253 17.585); --chart-3: oklch(0.586 0.253 17.585);
--chart-4: oklch(0.514 0.222 16.935); --chart-4: oklch(0.514 0.222 16.935);
--chart-5: oklch(0.455 0.188 13.697); --chart-5: oklch(0.455 0.188 13.697);
--sidebar: oklch(0.985 0 0); --sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823); --sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.586 0.253 17.585); --sidebar-primary: oklch(0.586 0.253 17.585);
--sidebar-primary-foreground: oklch(0.969 0.015 12.422); --sidebar-primary-foreground: oklch(0.969 0.015 12.422);
--sidebar-accent: oklch(0.967 0.001 286.375); --sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885); --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32); --sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.712 0.194 13.428); --sidebar-ring: oklch(0.712 0.194 13.428);
} }
:root[data-theme='dark'] { :root[data-theme='dark'] {
--background: oklch(0.141 0.005 285.823); --background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885); --card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885); --popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.645 0.246 16.439); --primary: oklch(0.645 0.246 16.439);
--primary-foreground: oklch(0.969 0.015 12.422); --primary-foreground: oklch(0.969 0.015 12.422);
--secondary: oklch(0.274 0.006 286.033); --secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033); --muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067); --muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033); --accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%); --border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%); --input: oklch(1 0 0 / 15%);
--ring: oklch(0.41 0.159 10.272); --ring: oklch(0.41 0.159 10.272);
--chart-1: oklch(0.81 0.117 11.638); --chart-1: oklch(0.81 0.117 11.638);
--chart-2: oklch(0.645 0.246 16.439); --chart-2: oklch(0.645 0.246 16.439);
--chart-3: oklch(0.586 0.253 17.585); --chart-3: oklch(0.586 0.253 17.585);
--chart-4: oklch(0.514 0.222 16.935); --chart-4: oklch(0.514 0.222 16.935);
--chart-5: oklch(0.455 0.188 13.697); --chart-5: oklch(0.455 0.188 13.697);
--sidebar: oklch(0.21 0.006 285.885); --sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.645 0.246 16.439); --sidebar-primary: oklch(0.645 0.246 16.439);
--sidebar-primary-foreground: oklch(0.969 0.015 12.422); --sidebar-primary-foreground: oklch(0.969 0.015 12.422);
--sidebar-accent: oklch(0.274 0.006 286.033); --sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.41 0.159 10.272); --sidebar-ring: oklch(0.41 0.159 10.272);
} }
+63 -63
View File
@@ -1,68 +1,68 @@
:root[data-theme='light'] { :root[data-theme='light'] {
--radius: 0.65rem; --radius: 0.65rem;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823); --foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823); --card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823); --popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.541 0.281 293.009); --primary: oklch(0.541 0.281 293.009);
--primary-foreground: oklch(0.969 0.016 293.756); --primary-foreground: oklch(0.969 0.016 293.756);
--secondary: oklch(0.967 0.001 286.375); --secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885); --secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375); --muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938); --muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375); --accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885); --accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32); --border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32); --input: oklch(0.92 0.004 286.32);
--ring: oklch(0.702 0.183 293.541); --ring: oklch(0.702 0.183 293.541);
--chart-1: oklch(0.811 0.111 293.571); --chart-1: oklch(0.811 0.111 293.571);
--chart-2: oklch(0.606 0.25 292.717); --chart-2: oklch(0.606 0.25 292.717);
--chart-3: oklch(0.541 0.281 293.009); --chart-3: oklch(0.541 0.281 293.009);
--chart-4: oklch(0.491 0.27 292.581); --chart-4: oklch(0.491 0.27 292.581);
--chart-5: oklch(0.432 0.232 292.759); --chart-5: oklch(0.432 0.232 292.759);
--sidebar: oklch(0.985 0 0); --sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823); --sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.541 0.281 293.009); --sidebar-primary: oklch(0.541 0.281 293.009);
--sidebar-primary-foreground: oklch(0.969 0.016 293.756); --sidebar-primary-foreground: oklch(0.969 0.016 293.756);
--sidebar-accent: oklch(0.967 0.001 286.375); --sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885); --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32); --sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.702 0.183 293.541); --sidebar-ring: oklch(0.702 0.183 293.541);
} }
:root[data-theme='dark'] { :root[data-theme='dark'] {
--background: oklch(0.141 0.005 285.823); --background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885); --card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885); --popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.606 0.25 292.717); --primary: oklch(0.606 0.25 292.717);
--primary-foreground: oklch(0.969 0.016 293.756); --primary-foreground: oklch(0.969 0.016 293.756);
--secondary: oklch(0.274 0.006 286.033); --secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033); --muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067); --muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033); --accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%); --border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%); --input: oklch(1 0 0 / 15%);
--ring: oklch(0.38 0.189 293.745); --ring: oklch(0.38 0.189 293.745);
--chart-1: oklch(0.811 0.111 293.571); --chart-1: oklch(0.811 0.111 293.571);
--chart-2: oklch(0.606 0.25 292.717); --chart-2: oklch(0.606 0.25 292.717);
--chart-3: oklch(0.541 0.281 293.009); --chart-3: oklch(0.541 0.281 293.009);
--chart-4: oklch(0.491 0.27 292.581); --chart-4: oklch(0.491 0.27 292.581);
--chart-5: oklch(0.432 0.232 292.759); --chart-5: oklch(0.432 0.232 292.759);
--sidebar: oklch(0.21 0.006 285.885); --sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.606 0.25 292.717); --sidebar-primary: oklch(0.606 0.25 292.717);
--sidebar-primary-foreground: oklch(0.969 0.016 293.756); --sidebar-primary-foreground: oklch(0.969 0.016 293.756);
--sidebar-accent: oklch(0.274 0.006 286.033); --sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.38 0.189 293.745); --sidebar-ring: oklch(0.38 0.189 293.745);
} }
+63 -63
View File
@@ -1,68 +1,68 @@
:root[data-theme='light'] { :root[data-theme='light'] {
--radius: 0.65rem; --radius: 0.65rem;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823); --foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823); --card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823); --popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.852 0.199 91.936); --primary: oklch(0.852 0.199 91.936);
--primary-foreground: oklch(0.421 0.095 57.708); --primary-foreground: oklch(0.421 0.095 57.708);
--secondary: oklch(0.967 0.001 286.375); --secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885); --secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375); --muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938); --muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375); --accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885); --accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32); --border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32); --input: oklch(0.92 0.004 286.32);
--ring: oklch(0.852 0.199 91.936); --ring: oklch(0.852 0.199 91.936);
--chart-1: oklch(0.905 0.182 98.111); --chart-1: oklch(0.905 0.182 98.111);
--chart-2: oklch(0.795 0.184 86.047); --chart-2: oklch(0.795 0.184 86.047);
--chart-3: oklch(0.681 0.162 75.834); --chart-3: oklch(0.681 0.162 75.834);
--chart-4: oklch(0.554 0.135 66.442); --chart-4: oklch(0.554 0.135 66.442);
--chart-5: oklch(0.476 0.114 61.907); --chart-5: oklch(0.476 0.114 61.907);
--sidebar: oklch(0.985 0 0); --sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823); --sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.681 0.162 75.834); --sidebar-primary: oklch(0.681 0.162 75.834);
--sidebar-primary-foreground: oklch(0.987 0.026 102.212); --sidebar-primary-foreground: oklch(0.987 0.026 102.212);
--sidebar-accent: oklch(0.967 0.001 286.375); --sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885); --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32); --sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.852 0.199 91.936); --sidebar-ring: oklch(0.852 0.199 91.936);
} }
:root[data-theme='dark'] { :root[data-theme='dark'] {
--background: oklch(0.141 0.005 285.823); --background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885); --card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885); --popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.795 0.184 86.047); --primary: oklch(0.795 0.184 86.047);
--primary-foreground: oklch(0.421 0.095 57.708); --primary-foreground: oklch(0.421 0.095 57.708);
--secondary: oklch(0.274 0.006 286.033); --secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033); --muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067); --muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033); --accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%); --border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%); --input: oklch(1 0 0 / 15%);
--ring: oklch(0.421 0.095 57.708); --ring: oklch(0.421 0.095 57.708);
--chart-1: oklch(0.905 0.182 98.111); --chart-1: oklch(0.905 0.182 98.111);
--chart-2: oklch(0.795 0.184 86.047); --chart-2: oklch(0.795 0.184 86.047);
--chart-3: oklch(0.681 0.162 75.834); --chart-3: oklch(0.681 0.162 75.834);
--chart-4: oklch(0.554 0.135 66.442); --chart-4: oklch(0.554 0.135 66.442);
--chart-5: oklch(0.476 0.114 61.907); --chart-5: oklch(0.476 0.114 61.907);
--sidebar: oklch(0.21 0.006 285.885); --sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.795 0.184 86.047); --sidebar-primary: oklch(0.795 0.184 86.047);
--sidebar-primary-foreground: oklch(0.987 0.026 102.212); --sidebar-primary-foreground: oklch(0.987 0.026 102.212);
--sidebar-accent: oklch(0.274 0.006 286.033); --sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.421 0.095 57.708); --sidebar-ring: oklch(0.421 0.095 57.708);
} }
@@ -80,9 +80,19 @@ describe('graphLayoutWorker message protocol', () => {
harness.dispatch({ harness.dispatch({
requestId: 11, requestId: 11,
nodes: [{ id: 'n1', attributes: { x: 0, y: 0 } }, { id: 'n2', attributes: { x: 2, y: 2 } }], nodes: [
edges: [{ key: 'n1__n2', source: 'n1', target: 'n2', attributes: {} }], { id: 'n1', attributes: { x: 0, y: 0 } },
settings: { layoutIterations: 300, layoutSpacing: 60, deltaSpacing: 0, reinitialize: false } { id: 'n2', attributes: { x: 2, y: 2 } }
],
edges: [
{ key: 'n1__n2', source: 'n1', target: 'n2', attributes: {} }
],
settings: {
layoutIterations: 300,
layoutSpacing: 60,
deltaSpacing: 0,
reinitialize: false
}
}); });
expect(harness.sent).toHaveLength(1); expect(harness.sent).toHaveLength(1);
@@ -98,8 +108,15 @@ describe('graphLayoutWorker message protocol', () => {
harness.dispatch({ harness.dispatch({
requestId: 12, requestId: 12,
nodes: [{ id: 'n1', attributes: { x: 0, y: 0 } }], nodes: [{ id: 'n1', attributes: { x: 0, y: 0 } }],
edges: [{ key: 'n1__n2', source: 'n1', target: 'n2', attributes: {} }], edges: [
settings: { layoutIterations: 300, layoutSpacing: 60, deltaSpacing: 0, reinitialize: false } { key: 'n1__n2', source: 'n1', target: 'n2', attributes: {} }
],
settings: {
layoutIterations: 300,
layoutSpacing: 60,
deltaSpacing: 0,
reinitialize: false
}
}); });
expect(harness.sent).toHaveLength(1); expect(harness.sent).toHaveLength(1);
@@ -596,15 +596,15 @@
const location = parseLocation(instanceData.location); const location = parseLocation(instanceData.location);
return ` return `
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<div style="width: 10px; height: 55px; background-color: ${color}; margin-right: 6px;"></div> <div style="width: 10px; height: 55px; background-color: ${color}; margin-right: 6px;"></div>
<div> <div>
<div>${name} #${location.instanceName} ${location.accessTypeName}</div> <div>${name} #${location.instanceName} ${location.accessTypeName}</div>
<div>${formattedJoinDateTime} - ${formattedLeftDateTime}</div> <div>${formattedJoinDateTime} - ${formattedLeftDateTime}</div>
<div>${timeString}</div> <div>${timeString}</div>
</div>
</div> </div>
</div> `;
`;
}; };
const format = dtHour12.value ? 'hh:mm A' : 'HH:mm'; const format = dtHour12.value ? 'hh:mm A' : 'HH:mm';
@@ -359,15 +359,15 @@
const color = param.color; const color = param.color;
return ` return `
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<div style="width: 10px; height: 55px; background-color: ${color}; margin-right: 6px;"></div> <div style="width: 10px; height: 55px; background-color: ${color}; margin-right: 6px;"></div>
<div> <div>
<div>${instanceData.display_name} ${friendOrFavIcon(instanceData.display_name)}</div> <div>${instanceData.display_name} ${friendOrFavIcon(instanceData.display_name)}</div>
<div>${formattedJoinDateTime} - ${formattedLeftDateTime}</div> <div>${formattedJoinDateTime} - ${formattedLeftDateTime}</div>
<div>${timeString}</div> <div>${timeString}</div>
</div>
</div> </div>
</div> `;
`;
}; };
const format = dtHour12.value ? 'hh:mm A' : 'HH:mm'; const format = dtHour12.value ? 'hh:mm A' : 'HH:mm';
+2 -10
View File
@@ -403,11 +403,7 @@
const edgeCurvatureModel = computed({ const edgeCurvatureModel = computed({
get: () => [layoutSettings.edgeCurvature], get: () => [layoutSettings.edgeCurvature],
set: (value) => { set: (value) => {
const next = clampNumber( const next = clampNumber(value?.[0] ?? layoutSettings.edgeCurvature, EDGE_CURVATURE_MIN, EDGE_CURVATURE_MAX);
value?.[0] ?? layoutSettings.edgeCurvature,
EDGE_CURVATURE_MIN,
EDGE_CURVATURE_MAX
);
layoutSettings.edgeCurvature = Number(next.toFixed(2)); layoutSettings.edgeCurvature = Number(next.toFixed(2));
} }
}); });
@@ -468,11 +464,7 @@
layoutSettings.layoutIterations = clampNumber(iterations, LAYOUT_ITERATIONS_MIN, LAYOUT_ITERATIONS_MAX); layoutSettings.layoutIterations = clampNumber(iterations, LAYOUT_ITERATIONS_MIN, LAYOUT_ITERATIONS_MAX);
layoutSettings.layoutSpacing = clampNumber(spacing, LAYOUT_SPACING_MIN, LAYOUT_SPACING_MAX); layoutSettings.layoutSpacing = clampNumber(spacing, LAYOUT_SPACING_MIN, LAYOUT_SPACING_MAX);
layoutSettings.edgeCurvature = clampNumber(curvature, EDGE_CURVATURE_MIN, EDGE_CURVATURE_MAX); layoutSettings.edgeCurvature = clampNumber(curvature, EDGE_CURVATURE_MIN, EDGE_CURVATURE_MAX);
layoutSettings.communitySeparation = clampNumber( layoutSettings.communitySeparation = clampNumber(separation, COMMUNITY_SEPARATION_MIN, COMMUNITY_SEPARATION_MAX);
separation,
COMMUNITY_SEPARATION_MIN,
COMMUNITY_SEPARATION_MAX
);
lastLayoutSpacing = layoutSettings.layoutSpacing; lastLayoutSpacing = layoutSettings.layoutSpacing;
} }
+9 -2
View File
@@ -90,7 +90,12 @@ function runLayout(data) {
graph.addNode(node.id, node.attributes); graph.addNode(node.id, node.attributes);
} }
for (const edge of edges) { for (const edge of edges) {
graph.addEdgeWithKey(edge.key, edge.source, edge.target, edge.attributes); graph.addEdgeWithKey(
edge.key,
edge.source,
edge.target,
edge.attributes
);
} }
const reinitialize = settings.reinitialize ?? false; const reinitialize = settings.reinitialize ?? false;
@@ -108,7 +113,9 @@ function runLayout(data) {
LAYOUT_SPACING_MIN, LAYOUT_SPACING_MIN,
LAYOUT_SPACING_MAX LAYOUT_SPACING_MAX
); );
const t = (spacing - LAYOUT_SPACING_MIN) / (LAYOUT_SPACING_MAX - LAYOUT_SPACING_MIN); const t =
(spacing - LAYOUT_SPACING_MIN) /
(LAYOUT_SPACING_MAX - LAYOUT_SPACING_MIN);
const clampedT = clampNumber(t, 0, 1); const clampedT = clampNumber(t, 0, 1);
const deltaSpacing = settings.deltaSpacing ?? 0; const deltaSpacing = settings.deltaSpacing ?? 0;
@@ -17,7 +17,11 @@
:key="option.key" :key="option.key"
type="button" type="button"
class="flex items-center gap-2 rounded-md border p-2 text-left text-sm hover:bg-accent" class="flex items-center gap-2 rounded-md border p-2 text-left text-sm hover:bg-accent"
:class="option.key === currentPanelKey ? 'border-primary bg-primary/5 ring-1 ring-primary/40' : 'border-primary/20'" :class="
option.key === currentPanelKey
? 'border-primary bg-primary/5 ring-1 ring-primary/40'
: 'border-primary/20'
"
@click="handleSelectWidget(option)"> @click="handleSelectWidget(option)">
<i :class="option.icon" class="text-base"></i> <i :class="option.icon" class="text-base"></i>
<span>{{ t(option.labelKey) }}</span> <span>{{ t(option.labelKey) }}</span>
@@ -36,7 +40,11 @@
:key="option.key" :key="option.key"
type="button" type="button"
class="flex items-center gap-2 rounded-md border p-2 text-left text-sm hover:bg-accent" class="flex items-center gap-2 rounded-md border p-2 text-left text-sm hover:bg-accent"
:class="option.key === currentPanelKey ? 'border-primary bg-primary/5 ring-1 ring-primary/40' : ''" :class="
option.key === currentPanelKey
? 'border-primary bg-primary/5 ring-1 ring-primary/40'
: ''
"
@click="emit('select', option.key)"> @click="emit('select', option.key)">
<i :class="option.icon" class="text-base"></i> <i :class="option.icon" class="text-base"></i>
<span>{{ t(option.labelKey) }}</span> <span>{{ t(option.labelKey) }}</span>
@@ -29,10 +29,20 @@ export const panelComponentMap = {
moderation: Moderation, moderation: Moderation,
notification: Notification, notification: Notification,
'my-avatars': MyAvatars, 'my-avatars': MyAvatars,
'charts-instance': defineAsyncComponent(() => import('../../Charts/components/InstanceActivity.vue')), 'charts-instance': defineAsyncComponent(
'charts-mutual': defineAsyncComponent(() => import('../../Charts/components/MutualFriends.vue')), () => import('../../Charts/components/InstanceActivity.vue')
),
'charts-mutual': defineAsyncComponent(
() => import('../../Charts/components/MutualFriends.vue')
),
tools: Tools, tools: Tools,
'widget:feed': defineAsyncComponent(() => import('../widgets/FeedWidget.vue')), 'widget:feed': defineAsyncComponent(
'widget:game-log': defineAsyncComponent(() => import('../widgets/GameLogWidget.vue')), () => import('../widgets/FeedWidget.vue')
'widget:instance': defineAsyncComponent(() => import('../widgets/InstanceWidget.vue')) ),
'widget:game-log': defineAsyncComponent(
() => import('../widgets/GameLogWidget.vue')
),
'widget:instance': defineAsyncComponent(
() => import('../widgets/InstanceWidget.vue')
)
}; };
@@ -160,15 +160,7 @@
import WidgetHeader from './WidgetHeader.vue'; import WidgetHeader from './WidgetHeader.vue';
import { Table, TableBody, TableRow, TableCell } from '@/components/ui/table'; import { Table, TableBody, TableRow, TableCell } from '@/components/ui/table';
const GAMELOG_TYPES = [ const GAMELOG_TYPES = ['Location', 'OnPlayerJoined', 'OnPlayerLeft', 'VideoPlay', 'PortalSpawn', 'Event', 'External'];
'Location',
'OnPlayerJoined',
'OnPlayerLeft',
'VideoPlay',
'PortalSpawn',
'Event',
'External'
];
const props = defineProps({ const props = defineProps({
config: { config: {
+2 -4
View File
@@ -668,9 +668,7 @@
if (!activeRemoteGroup.value || !currentRemoteFavorites.value.length) { if (!activeRemoteGroup.value || !currentRemoteFavorites.value.length) {
return false; return false;
} }
return currentRemoteFavorites.value return currentRemoteFavorites.value.map((fav) => fav.id).every((id) => selectedFavoriteAvatars.value.includes(id));
.map((fav) => fav.id)
.every((id) => selectedFavoriteAvatars.value.includes(id));
}); });
watch( watch(
@@ -1226,7 +1224,7 @@
modalStore modalStore
.confirm({ .confirm({
description: `Are you sure you want to unfavorite ${total} favorites? description: `Are you sure you want to unfavorite ${total} favorites?
This action cannot be undone.`, This action cannot be undone.`,
title: `Delete ${total} favorites?` title: `Delete ${total} favorites?`
}) })
.then(({ ok }) => { .then(({ ok }) => {
+1 -3
View File
@@ -553,9 +553,7 @@
if (!activeRemoteGroup.value || !currentFriendFavorites.value.length) { if (!activeRemoteGroup.value || !currentFriendFavorites.value.length) {
return false; return false;
} }
return currentFriendFavorites.value return currentFriendFavorites.value.map((fav) => fav.id).every((id) => selectedFavoriteFriends.value.includes(id));
.map((fav) => fav.id)
.every((id) => selectedFavoriteFriends.value.includes(id));
}); });
watch( watch(
+2 -4
View File
@@ -694,9 +694,7 @@
if (!activeRemoteGroup.value || !currentRemoteFavorites.value.length) { if (!activeRemoteGroup.value || !currentRemoteFavorites.value.length) {
return false; return false;
} }
return currentRemoteFavorites.value return currentRemoteFavorites.value.map((fav) => fav.id).every((id) => selectedFavoriteWorlds.value.includes(id));
.map((fav) => fav.id)
.every((id) => selectedFavoriteWorlds.value.includes(id));
}); });
watch( watch(
@@ -838,7 +836,7 @@
modalStore modalStore
.confirm({ .confirm({
description: `Are you sure you want to unfavorite ${total} favorites? description: `Are you sure you want to unfavorite ${total} favorites?
This action cannot be undone.`, This action cannot be undone.`,
title: `Delete ${total} favorites?` title: `Delete ${total} favorites?`
}) })
.then(({ ok }) => { .then(({ ok }) => {
@@ -35,11 +35,13 @@ vi.mock('../../../../stores', () => ({
vi.mock('../../../../coordinators/avatarCoordinator', () => ({ vi.mock('../../../../coordinators/avatarCoordinator', () => ({
showAvatarDialog: (...args) => mocks.showAvatarDialog(...args), showAvatarDialog: (...args) => mocks.showAvatarDialog(...args),
selectAvatarWithConfirmation: (...args) => mocks.selectAvatarWithConfirmation(...args) selectAvatarWithConfirmation: (...args) =>
mocks.selectAvatarWithConfirmation(...args)
})); }));
vi.mock('../../../../coordinators/favoriteCoordinator', () => ({ vi.mock('../../../../coordinators/favoriteCoordinator', () => ({
removeLocalAvatarFavorite: (...args) => mocks.removeLocalAvatarFavorite(...args) removeLocalAvatarFavorite: (...args) =>
mocks.removeLocalAvatarFavorite(...args)
})); }));
vi.mock('../../../../api', () => ({ vi.mock('../../../../api', () => ({
@@ -51,7 +53,8 @@ vi.mock('../../../../api', () => ({
vi.mock('@/components/ui/item', () => ({ vi.mock('@/components/ui/item', () => ({
Item: { Item: {
emits: ['click'], emits: ['click'],
template: '<div data-testid="item" @click="$emit(\'click\', $event)"><slot /></div>' template:
'<div data-testid="item" @click="$emit(\'click\', $event)"><slot /></div>'
}, },
ItemActions: { template: '<div><slot /></div>' }, ItemActions: { template: '<div><slot /></div>' },
ItemMedia: { template: '<div><slot /></div>' }, ItemMedia: { template: '<div><slot /></div>' },
@@ -64,15 +67,20 @@ vi.mock('@/components/ui/avatar', () => ({
Avatar: { template: '<div data-testid="avatar"><slot /></div>' }, Avatar: { template: '<div data-testid="avatar"><slot /></div>' },
AvatarImage: { AvatarImage: {
props: ['src'], props: ['src'],
template: '<img data-testid="avatar-image" :src="src" :class="$attrs.class" />' template:
'<img data-testid="avatar-image" :src="src" :class="$attrs.class" />'
}, },
AvatarFallback: { template: '<span data-testid="avatar-fallback" :class="$attrs.class"><slot /></span>' } AvatarFallback: {
template:
'<span data-testid="avatar-fallback" :class="$attrs.class"><slot /></span>'
}
})); }));
vi.mock('@/components/ui/button', () => ({ vi.mock('@/components/ui/button', () => ({
Button: { Button: {
emits: ['click'], emits: ['click'],
template: '<button data-testid="button" @click="$emit(\'click\', $event)"><slot /></button>' template:
'<button data-testid="button" @click="$emit(\'click\', $event)"><slot /></button>'
} }
})); }));
@@ -92,7 +100,8 @@ vi.mock('@/components/ui/context-menu', () => ({
ContextMenuSeparator: { template: '<hr />' }, ContextMenuSeparator: { template: '<hr />' },
ContextMenuItem: { ContextMenuItem: {
emits: ['click'], emits: ['click'],
template: '<button data-testid="ctx-item" @click="$emit(\'click\')"><slot /></button>' template:
'<button data-testid="ctx-item" @click="$emit(\'click\')"><slot /></button>'
} }
})); }));
@@ -103,7 +112,8 @@ vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenuSeparator: { template: '<hr />' }, DropdownMenuSeparator: { template: '<hr />' },
DropdownMenuItem: { DropdownMenuItem: {
emits: ['click'], emits: ['click'],
template: '<button data-testid="dd-item" @click="$emit(\'click\')"><slot /></button>' template:
'<button data-testid="dd-item" @click="$emit(\'click\')"><slot /></button>'
} }
})); }));
@@ -175,16 +185,26 @@ describe('FavoritesAvatarItem.vue', () => {
const wrapper = mountItem(); const wrapper = mountItem();
expect(wrapper.get('[data-testid="item"]').classes()).toEqual( expect(wrapper.get('[data-testid="item"]').classes()).toEqual(
expect.arrayContaining(['favorites-item', 'hover:bg-muted', 'x-hover-list']) expect.arrayContaining([
'favorites-item',
'hover:bg-muted',
'x-hover-list'
])
); );
}); });
it('uses rounded avatar shell and 128 thumbnail image', () => { it('uses rounded avatar shell and 128 thumbnail image', () => {
const wrapper = mountItem(); const wrapper = mountItem();
expect(wrapper.get('[data-testid="avatar"]').classes()).toEqual(expect.arrayContaining(['rounded-sm', 'size-full'])); expect(wrapper.get('[data-testid="avatar"]').classes()).toEqual(
expect(wrapper.get('[data-testid="avatar-image"]').attributes('src')).toContain('avatar_128.png'); expect.arrayContaining(['rounded-sm', 'size-full'])
expect(wrapper.get('[data-testid="avatar-fallback"]').classes()).toContain('rounded-sm'); );
expect(
wrapper.get('[data-testid="avatar-image"]').attributes('src')
).toContain('avatar_128.png');
expect(
wrapper.get('[data-testid="avatar-fallback"]').classes()
).toContain('rounded-sm');
}); });
it('shows fallback text when thumbnail is missing', () => { it('shows fallback text when thumbnail is missing', () => {
@@ -200,8 +220,12 @@ describe('FavoritesAvatarItem.vue', () => {
} }
}); });
expect(wrapper.find('[data-testid="avatar-image"]').exists()).toBe(false); expect(wrapper.find('[data-testid="avatar-image"]').exists()).toBe(
expect(wrapper.get('[data-testid="avatar-fallback"]').text()).toContain('B'); false
);
expect(wrapper.get('[data-testid="avatar-fallback"]').text()).toContain(
'B'
);
}); });
it('uses local delete flow for local favorites', async () => { it('uses local delete flow for local favorites', async () => {
@@ -217,7 +241,10 @@ describe('FavoritesAvatarItem.vue', () => {
await clickMenuItem(wrapper, 'view.favorite.delete_tooltip'); await clickMenuItem(wrapper, 'view.favorite.delete_tooltip');
expect(mocks.removeLocalAvatarFavorite).toHaveBeenCalledWith('avtr_local', 'LocalGroup'); expect(mocks.removeLocalAvatarFavorite).toHaveBeenCalledWith(
'avtr_local',
'LocalGroup'
);
expect(mocks.deleteFavorite).not.toHaveBeenCalled(); expect(mocks.deleteFavorite).not.toHaveBeenCalled();
}); });
@@ -226,7 +253,9 @@ describe('FavoritesAvatarItem.vue', () => {
await clickMenuItem(wrapper, 'view.favorite.unfavorite_tooltip'); await clickMenuItem(wrapper, 'view.favorite.unfavorite_tooltip');
expect(mocks.deleteFavorite).toHaveBeenCalledWith({ objectId: 'avtr_1' }); expect(mocks.deleteFavorite).toHaveBeenCalledWith({
objectId: 'avtr_1'
});
expect(mocks.removeLocalAvatarFavorite).not.toHaveBeenCalled(); expect(mocks.removeLocalAvatarFavorite).not.toHaveBeenCalled();
}); });
}); });
@@ -33,13 +33,15 @@ vi.mock('../../../../stores', () => ({
vi.mock('../../../../coordinators/avatarCoordinator', () => ({ vi.mock('../../../../coordinators/avatarCoordinator', () => ({
showAvatarDialog: (...args) => mocks.showAvatarDialog(...args), showAvatarDialog: (...args) => mocks.showAvatarDialog(...args),
selectAvatarWithConfirmation: (...args) => mocks.selectAvatarWithConfirmation(...args) selectAvatarWithConfirmation: (...args) =>
mocks.selectAvatarWithConfirmation(...args)
})); }));
vi.mock('@/components/ui/item', () => ({ vi.mock('@/components/ui/item', () => ({
Item: { Item: {
emits: ['click'], emits: ['click'],
template: '<div data-testid="item" @click="$emit(\'click\', $event)"><slot /></div>' template:
'<div data-testid="item" @click="$emit(\'click\', $event)"><slot /></div>'
}, },
ItemActions: { template: '<div><slot /></div>' }, ItemActions: { template: '<div><slot /></div>' },
ItemMedia: { template: '<div><slot /></div>' }, ItemMedia: { template: '<div><slot /></div>' },
@@ -52,15 +54,20 @@ vi.mock('@/components/ui/avatar', () => ({
Avatar: { template: '<div data-testid="avatar"><slot /></div>' }, Avatar: { template: '<div data-testid="avatar"><slot /></div>' },
AvatarImage: { AvatarImage: {
props: ['src'], props: ['src'],
template: '<img data-testid="avatar-image" :src="src" :class="$attrs.class" />' template:
'<img data-testid="avatar-image" :src="src" :class="$attrs.class" />'
}, },
AvatarFallback: { template: '<span data-testid="avatar-fallback" :class="$attrs.class"><slot /></span>' } AvatarFallback: {
template:
'<span data-testid="avatar-fallback" :class="$attrs.class"><slot /></span>'
}
})); }));
vi.mock('@/components/ui/button', () => ({ vi.mock('@/components/ui/button', () => ({
Button: { Button: {
emits: ['click'], emits: ['click'],
template: '<button data-testid="button" @click="$emit(\'click\', $event)"><slot /></button>' template:
'<button data-testid="button" @click="$emit(\'click\', $event)"><slot /></button>'
} }
})); }));
@@ -71,7 +78,8 @@ vi.mock('@/components/ui/context-menu', () => ({
ContextMenuSeparator: { template: '<hr />' }, ContextMenuSeparator: { template: '<hr />' },
ContextMenuItem: { ContextMenuItem: {
emits: ['click'], emits: ['click'],
template: '<button data-testid="ctx-item" @click="$emit(\'click\')"><slot /></button>' template:
'<button data-testid="ctx-item" @click="$emit(\'click\')"><slot /></button>'
} }
})); }));
@@ -82,7 +90,8 @@ vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenuSeparator: { template: '<hr />' }, DropdownMenuSeparator: { template: '<hr />' },
DropdownMenuItem: { DropdownMenuItem: {
emits: ['click'], emits: ['click'],
template: '<button data-testid="dd-item" @click="$emit(\'click\')"><slot /></button>' template:
'<button data-testid="dd-item" @click="$emit(\'click\')"><slot /></button>'
} }
})); }));
@@ -142,16 +151,26 @@ describe('FavoritesAvatarLocalHistoryItem.vue', () => {
const wrapper = mountItem(); const wrapper = mountItem();
expect(wrapper.get('[data-testid="item"]').classes()).toEqual( expect(wrapper.get('[data-testid="item"]').classes()).toEqual(
expect.arrayContaining(['favorites-item', 'hover:bg-muted', 'x-hover-list']) expect.arrayContaining([
'favorites-item',
'hover:bg-muted',
'x-hover-list'
])
); );
}); });
it('uses rounded avatar shell and 128 thumbnail image', () => { it('uses rounded avatar shell and 128 thumbnail image', () => {
const wrapper = mountItem(); const wrapper = mountItem();
expect(wrapper.get('[data-testid="avatar"]').classes()).toEqual(expect.arrayContaining(['rounded-sm', 'size-full'])); expect(wrapper.get('[data-testid="avatar"]').classes()).toEqual(
expect(wrapper.get('[data-testid="avatar-image"]').attributes('src')).toContain('history_128.png'); expect.arrayContaining(['rounded-sm', 'size-full'])
expect(wrapper.get('[data-testid="avatar-fallback"]').classes()).toContain('rounded-sm'); );
expect(
wrapper.get('[data-testid="avatar-image"]').attributes('src')
).toContain('history_128.png');
expect(
wrapper.get('[data-testid="avatar-fallback"]').classes()
).toContain('rounded-sm');
}); });
it('shows fallback text when thumbnail is missing', () => { it('shows fallback text when thumbnail is missing', () => {
@@ -163,8 +182,12 @@ describe('FavoritesAvatarLocalHistoryItem.vue', () => {
} }
}); });
expect(wrapper.find('[data-testid="avatar-image"]').exists()).toBe(false); expect(wrapper.find('[data-testid="avatar-image"]').exists()).toBe(
expect(wrapper.get('[data-testid="avatar-fallback"]').text()).toContain('C'); false
);
expect(wrapper.get('[data-testid="avatar-fallback"]').text()).toContain(
'C'
);
}); });
it('runs select-avatar action from menu', async () => { it('runs select-avatar action from menu', async () => {
@@ -172,6 +195,8 @@ describe('FavoritesAvatarLocalHistoryItem.vue', () => {
await clickMenuItem(wrapper, 'view.favorite.select_avatar_tooltip'); await clickMenuItem(wrapper, 'view.favorite.select_avatar_tooltip');
expect(mocks.selectAvatarWithConfirmation).toHaveBeenCalledWith('avtr_hist_1'); expect(mocks.selectAvatarWithConfirmation).toHaveBeenCalledWith(
'avtr_hist_1'
);
}); });
}); });
@@ -79,7 +79,8 @@ vi.mock('../../../../api', () => ({
})); }));
vi.mock('../../../../coordinators/favoriteCoordinator', () => ({ vi.mock('../../../../coordinators/favoriteCoordinator', () => ({
removeLocalFriendFavorite: (...args) => mocks.removeLocalFriendFavorite(...args) removeLocalFriendFavorite: (...args) =>
mocks.removeLocalFriendFavorite(...args)
})); }));
vi.mock('../../../../coordinators/userCoordinator', () => ({ vi.mock('../../../../coordinators/userCoordinator', () => ({
@@ -100,7 +101,11 @@ vi.mock('../../../../composables/useUserDisplay', () => ({
})); }));
vi.mock('../../../../shared/utils', () => ({ vi.mock('../../../../shared/utils', () => ({
parseLocation: () => ({ worldId: 'wrld_123', instanceId: '123', tag: '123~private' }), parseLocation: () => ({
worldId: 'wrld_123',
instanceId: '123',
tag: '123~private'
}),
isRealInstance: (...args) => mocks.isRealInstance(...args) isRealInstance: (...args) => mocks.isRealInstance(...args)
})); }));
@@ -113,7 +118,8 @@ vi.mock('../../../../components/Location.vue', () => ({
vi.mock('@/components/ui/item', () => ({ vi.mock('@/components/ui/item', () => ({
Item: { Item: {
emits: ['click'], emits: ['click'],
template: '<div data-testid="item" @click="$emit(\'click\', $event)"><slot /></div>' template:
'<div data-testid="item" @click="$emit(\'click\', $event)"><slot /></div>'
}, },
ItemActions: { template: '<div><slot /></div>' }, ItemActions: { template: '<div><slot /></div>' },
ItemMedia: { template: '<div><slot /></div>' }, ItemMedia: { template: '<div><slot /></div>' },
@@ -131,7 +137,8 @@ vi.mock('@/components/ui/avatar', () => ({
vi.mock('@/components/ui/button', () => ({ vi.mock('@/components/ui/button', () => ({
Button: { Button: {
emits: ['click'], emits: ['click'],
template: '<button data-testid="btn" @click="$emit(\'click\', $event)"><slot /></button>' template:
'<button data-testid="btn" @click="$emit(\'click\', $event)"><slot /></button>'
} }
})); }));
@@ -151,7 +158,8 @@ vi.mock('@/components/ui/context-menu', () => ({
ContextMenuSeparator: { template: '<hr />' }, ContextMenuSeparator: { template: '<hr />' },
ContextMenuItem: { ContextMenuItem: {
emits: ['click'], emits: ['click'],
template: '<button data-testid="context-item" @click="$emit(\'click\')"><slot /></button>' template:
'<button data-testid="context-item" @click="$emit(\'click\')"><slot /></button>'
} }
})); }));
@@ -162,7 +170,8 @@ vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenuSeparator: { template: '<hr />' }, DropdownMenuSeparator: { template: '<hr />' },
DropdownMenuItem: { DropdownMenuItem: {
emits: ['click'], emits: ['click'],
template: '<button data-testid="dropdown-item" @click="$emit(\'click\')"><slot /></button>' template:
'<button data-testid="dropdown-item" @click="$emit(\'click\')"><slot /></button>'
} }
})); }));
@@ -244,7 +253,11 @@ describe('FavoritesFriendItem.vue', () => {
const wrapper = mountItem(); const wrapper = mountItem();
expect(wrapper.get('[data-testid="item"]').classes()).toEqual( expect(wrapper.get('[data-testid="item"]').classes()).toEqual(
expect.arrayContaining(['favorites-item', 'hover:bg-muted', 'x-hover-list']) expect.arrayContaining([
'favorites-item',
'hover:bg-muted',
'x-hover-list'
])
); );
}); });
@@ -261,7 +274,10 @@ describe('FavoritesFriendItem.vue', () => {
await clickMenuItem(wrapper, 'view.favorite.delete_tooltip'); await clickMenuItem(wrapper, 'view.favorite.delete_tooltip');
expect(mocks.removeLocalFriendFavorite).toHaveBeenCalledWith('usr_1', 'Local'); expect(mocks.removeLocalFriendFavorite).toHaveBeenCalledWith(
'usr_1',
'Local'
);
expect(mocks.deleteFavorite).not.toHaveBeenCalled(); expect(mocks.deleteFavorite).not.toHaveBeenCalled();
}); });
@@ -270,7 +286,9 @@ describe('FavoritesFriendItem.vue', () => {
await clickMenuItem(wrapper, 'view.favorite.unfavorite_tooltip'); await clickMenuItem(wrapper, 'view.favorite.unfavorite_tooltip');
expect(mocks.deleteFavorite).toHaveBeenCalledWith({ objectId: 'usr_1' }); expect(mocks.deleteFavorite).toHaveBeenCalledWith({
objectId: 'usr_1'
});
expect(mocks.removeLocalFriendFavorite).not.toHaveBeenCalled(); expect(mocks.removeLocalFriendFavorite).not.toHaveBeenCalled();
}); });
@@ -279,8 +297,12 @@ describe('FavoritesFriendItem.vue', () => {
expect(wrapper.text()).toContain('dialog.user.actions.request_invite'); expect(wrapper.text()).toContain('dialog.user.actions.request_invite');
expect(wrapper.text()).toContain('dialog.user.actions.invite'); expect(wrapper.text()).toContain('dialog.user.actions.invite');
expect(wrapper.text()).toContain('dialog.user.info.launch_invite_tooltip'); expect(wrapper.text()).toContain(
expect(wrapper.text()).toContain('dialog.user.info.self_invite_tooltip'); 'dialog.user.info.launch_invite_tooltip'
);
expect(wrapper.text()).toContain(
'dialog.user.info.self_invite_tooltip'
);
}); });
it('hides invite/join actions when offline', () => { it('hides invite/join actions when offline', () => {
@@ -298,9 +320,15 @@ describe('FavoritesFriendItem.vue', () => {
} }
}); });
expect(wrapper.text()).not.toContain('dialog.user.actions.request_invite'); expect(wrapper.text()).not.toContain(
expect(wrapper.text()).not.toContain('dialog.user.info.launch_invite_tooltip'); 'dialog.user.actions.request_invite'
expect(wrapper.text()).not.toContain('dialog.user.info.self_invite_tooltip'); );
expect(wrapper.text()).not.toContain(
'dialog.user.info.launch_invite_tooltip'
);
expect(wrapper.text()).not.toContain(
'dialog.user.info.self_invite_tooltip'
);
}); });
it('triggers request invite action', async () => { it('triggers request invite action', async () => {
@@ -308,7 +336,10 @@ describe('FavoritesFriendItem.vue', () => {
await clickMenuItem(wrapper, 'dialog.user.actions.request_invite'); await clickMenuItem(wrapper, 'dialog.user.actions.request_invite');
expect(mocks.sendRequestInvite).toHaveBeenCalledWith({ platform: 'standalonewindows' }, 'usr_1'); expect(mocks.sendRequestInvite).toHaveBeenCalledWith(
{ platform: 'standalonewindows' },
'usr_1'
);
}); });
it('triggers join action', async () => { it('triggers join action', async () => {
@@ -316,6 +347,8 @@ describe('FavoritesFriendItem.vue', () => {
await clickMenuItem(wrapper, 'dialog.user.info.launch_invite_tooltip'); await clickMenuItem(wrapper, 'dialog.user.info.launch_invite_tooltip');
expect(mocks.showLaunchDialog).toHaveBeenCalledWith('wrld_aaa:1~private'); expect(mocks.showLaunchDialog).toHaveBeenCalledWith(
'wrld_aaa:1~private'
);
}); });
}); });

Some files were not shown because too many files have changed in this diff Show More