diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 76616ec5a7..1a1b30168a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,14 +10,14 @@ on: - packages/frontend/** - packages/sw/** - packages/misskey-js/** - - packages/shared/.eslintrc.js + - packages/shared/eslint.config.js pull_request: paths: - packages/backend/** - packages/frontend/** - packages/sw/** - packages/misskey-js/** - - packages/shared/.eslintrc.js + - packages/shared/eslint.config.js jobs: pnpm_install: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a28c9ef64..f62f966451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,21 +14,26 @@ - Fix: テーマプレビューが見れない問題を修正 ### Server -- チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正 - Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949) -- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006) -- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036) - Enhance: エンドポイント`clips/update`の必須項目を`clipId`のみに - Enhance: エンドポイント`admin/roles/update`の必須項目を`roleId`のみに - Enhance: エンドポイント`pages/update`の必須項目を`pageId`のみに - Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに - Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに - Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに +- Fix: チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正 +- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006) +- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036) - Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059) - Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正 - Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正 - Fix: 空文字列のリアクションはフォールバックされるように - Fix: リノートにリアクションできないように +- Fix: ユーザー名の前後に空白文字列がある場合は省略するように +- Fix: プロフィール編集時に名前を空白文字列のみにできる問題を修正 + +### Misskey.js +- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応) ## 2024.5.0 diff --git a/locales/index.js b/locales/index.js index 650e552337..c2738884eb 100644 --- a/locales/index.js +++ b/locales/index.js @@ -52,7 +52,11 @@ const primaries = { const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), ''); export function build() { - const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {}); + // vitestの挙動を調整するため、一度ローカル変数化する必要がある + // https://github.com/vitest-dev/vitest/issues/3988#issuecomment-1686599577 + // https://github.com/misskey-dev/misskey/pull/14057#issuecomment-2192833785 + const metaUrl = import.meta.url; + const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {}); // 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す const removeEmpty = (obj) => { diff --git a/package.json b/package.json index 0be1614bcb..92d77525c4 100644 --- a/package.json +++ b/package.json @@ -56,20 +56,22 @@ "js-yaml": "4.1.0", "postcss": "8.4.38", "tar": "6.2.1", - "terser": "5.30.3", - "typescript": "5.5.2", - "esbuild": "0.20.2", + "terser": "5.31.1", + "typescript": "5.5.3", + "esbuild": "0.22.0", "glob": "10.3.12" }, "devDependencies": { - "@types/node": "20.12.7", - "@typescript-eslint/eslint-plugin": "7.7.1", - "@typescript-eslint/parser": "7.7.1", + "@misskey-dev/eslint-plugin": "2.0.2", + "@types/node": "20.14.9", + "@typescript-eslint/eslint-plugin": "7.15.0", + "@typescript-eslint/parser": "7.15.0", "cross-env": "7.0.3", - "cypress": "13.7.3", - "eslint": "8.57.0", + "cypress": "13.13.0", + "eslint": "9.6.0", + "globals": "15.7.0", "ncp": "2.0.0", - "start-server-and-test": "2.0.3" + "start-server-and-test": "2.0.4" }, "optionalDependencies": { "@tensorflow/tfjs-core": "4.4.0" diff --git a/packages/backend/.eslintignore b/packages/backend/.eslintignore deleted file mode 100644 index 790eb90145..0000000000 --- a/packages/backend/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -/built -/.eslintrc.js -/@types/**/* diff --git a/packages/backend/.eslintrc.cjs b/packages/backend/.eslintrc.cjs deleted file mode 100644 index f9fe4814e6..0000000000 --- a/packages/backend/.eslintrc.cjs +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json', './test/tsconfig.json'], - }, - extends: [ - '../shared/.eslintrc.js', - ], - rules: { - 'import/order': ['warn', { - 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], - 'pathGroups': [ - { - 'pattern': '@/**', - 'group': 'external', - 'position': 'after' - } - ], - }], - 'no-restricted-globals': [ - 'error', - { - 'name': '__dirname', - 'message': 'Not in ESModule. Use `import.meta.url` instead.' - }, - { - 'name': '__filename', - 'message': 'Not in ESModule. Use `import.meta.url` instead.' - } - ] - }, -}; diff --git a/packages/backend/eslint.config.js b/packages/backend/eslint.config.js new file mode 100644 index 0000000000..318b7fd340 --- /dev/null +++ b/packages/backend/eslint.config.js @@ -0,0 +1,46 @@ +import tsParser from '@typescript-eslint/parser'; +import sharedConfig from '../shared/eslint.config.js'; + +export default [ + ...sharedConfig, + { + ignores: ['**/node_modules', 'built', '@types/**/*'], + }, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + parser: tsParser, + project: ['./tsconfig.json', './test/tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + 'import/order': ['warn', { + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'index', + 'object', + 'type', + ], + pathGroups: [{ + pattern: '@/**', + group: 'external', + position: 'after', + }], + }], + 'no-restricted-globals': ['error', { + name: '__dirname', + message: 'Not in ESModule. Use `import.meta.url` instead.', + }, { + name: '__filename', + message: 'Not in ESModule. Use `import.meta.url` instead.', + }], + }, + }, +]; diff --git a/packages/backend/package.json b/packages/backend/package.json index 66836c4a60..c2bba2bdda 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -65,43 +65,43 @@ "utf-8-validate": "6.0.3" }, "dependencies": { - "@aws-sdk/client-s3": "3.412.0", - "@aws-sdk/lib-storage": "3.412.0", - "@bull-board/api": "5.17.0", - "@bull-board/fastify": "5.17.0", - "@bull-board/ui": "5.17.0", + "@aws-sdk/client-s3": "3.600.0", + "@aws-sdk/lib-storage": "3.600.0", + "@bull-board/api": "5.20.5", + "@bull-board/fastify": "5.20.5", + "@bull-board/ui": "5.20.5", "@discordapp/twemoji": "15.0.3", "@fastify/accepts": "4.3.0", "@fastify/cookie": "9.3.1", "@fastify/cors": "9.0.1", "@fastify/express": "3.0.0", "@fastify/http-proxy": "9.5.0", - "@fastify/multipart": "8.2.0", - "@fastify/static": "7.0.3", + "@fastify/multipart": "8.3.0", + "@fastify/static": "7.0.4", "@fastify/view": "9.1.0", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.1.0", - "@napi-rs/canvas": "^0.1.52", - "@nestjs/common": "10.3.8", - "@nestjs/core": "10.3.8", - "@nestjs/testing": "10.3.8", + "@napi-rs/canvas": "^0.1.53", + "@nestjs/common": "10.3.10", + "@nestjs/core": "10.3.10", + "@nestjs/testing": "10.3.10", "@peertube/http-signature": "1.7.0", - "@sentry/node": "^8.5.0", - "@sentry/profiling-node": "^8.5.0", + "@sentry/node": "8.13.0", + "@sentry/profiling-node": "8.13.0", "@simplewebauthn/server": "10.0.0", "@sinonjs/fake-timers": "11.2.2", "@smithy/node-http-handler": "2.5.0", "@swc/cli": "0.3.12", - "@swc/core": "1.4.17", + "@swc/core": "1.6.6", "@twemoji/parser": "15.1.1", "accepts": "1.3.8", - "ajv": "8.13.0", + "ajv": "8.16.0", "archiver": "7.0.1", "async-mutex": "0.5.0", "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.2", - "bullmq": "5.7.8", + "bullmq": "5.8.3", "cacheable-lookup": "7.0.0", "cbor": "9.0.2", "chalk": "5.3.0", @@ -112,27 +112,27 @@ "content-disposition": "0.5.4", "date-fns": "2.30.0", "deep-email-validator": "0.1.21", - "fastify": "4.26.2", + "fastify": "4.28.1", "fastify-raw-body": "4.3.0", "feed": "4.2.2", "file-type": "19.0.0", - "fluent-ffmpeg": "2.1.2", + "fluent-ffmpeg": "2.1.3", "form-data": "4.0.0", - "got": "14.2.1", + "got": "14.4.1", "happy-dom": "10.0.3", "hpagent": "1.2.0", "htmlescape": "1.1.1", "http-link-header": "1.1.3", "ioredis": "5.4.1", - "ip-cidr": "3.1.0", + "ip-cidr": "4.0.1", "ipaddr.js": "2.2.0", - "is-svg": "5.0.0", + "is-svg": "5.0.1", "js-yaml": "4.1.0", - "jsdom": "24.0.0", + "jsdom": "24.1.0", "json5": "2.2.3", "jsonld": "8.3.2", "jsrsasign": "11.1.0", - "meilisearch": "0.38.0", + "meilisearch": "0.41.0", "mfm-js": "0.24.0", "microformats-parser": "2.0.2", "mime-types": "2.1.35", @@ -143,24 +143,24 @@ "nanoid": "5.0.7", "nested-property": "4.0.0", "node-fetch": "3.3.2", - "nodemailer": "6.9.13", + "nodemailer": "6.9.14", "nsfwjs": "2.4.2", "oauth": "0.10.0", "oauth2orize": "1.12.0", "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", - "otpauth": "9.2.3", + "otpauth": "9.3.1", "parse5": "7.1.2", - "pg": "8.11.5", + "pg": "8.12.0", "pkce-challenge": "4.1.0", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", - "pug": "3.0.2", + "pug": "3.0.3", "punycode": "2.3.1", "qrcode": "1.5.3", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.21.2", + "re2": "1.21.3", "redis-lock": "0.1.4", "reflect-metadata": "0.2.2", "rename": "1.0.4", @@ -168,27 +168,26 @@ "rxjs": "7.8.1", "sanitize-html": "2.13.0", "secure-json-parse": "2.7.0", - "sharp": "0.33.3", + "sharp": "0.33.4", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "systeminformation": "5.22.7", + "systeminformation": "5.22.11", "tinycolor2": "1.6.0", "tmp": "0.2.3", - "tsc-alias": "1.8.8", + "tsc-alias": "1.8.10", "tsconfig-paths": "4.2.0", "typeorm": "0.3.20", - "typescript": "5.5.2", + "typescript": "5.5.3", "ulid": "2.3.0", "vary": "1.1.2", "web-push": "3.6.7", - "ws": "8.17.0", + "ws": "8.17.1", "xev": "3.0.2" }, "devDependencies": { "@jest/globals": "29.7.0", - "@misskey-dev/eslint-plugin": "1.0.0", - "@nestjs/platform-express": "10.3.8", + "@nestjs/platform-express": "10.3.10", "@simplewebauthn/types": "10.0.0", "@swc/jest": "0.2.36", "@types/accepts": "1.3.7", @@ -198,21 +197,21 @@ "@types/color-convert": "2.0.3", "@types/content-disposition": "0.5.8", "@types/fluent-ffmpeg": "2.1.24", - "@types/htmlescape": "^1.1.3", - "@types/http-link-header": "1.0.5", + "@types/htmlescape": "1.1.3", + "@types/http-link-header": "1.0.7", "@types/jest": "29.5.12", "@types/js-yaml": "4.0.9", - "@types/jsdom": "21.1.6", - "@types/jsonld": "1.5.13", + "@types/jsdom": "21.1.7", + "@types/jsonld": "1.5.14", "@types/jsrsasign": "10.5.14", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "20.12.7", + "@types/node": "20.14.9", "@types/nodemailer": "6.4.15", - "@types/oauth": "0.9.4", + "@types/oauth": "0.9.5", "@types/oauth2orize": "1.11.5", "@types/oauth2orize-pkce": "0.1.2", - "@types/pg": "8.11.5", + "@types/pg": "8.11.6", "@types/pug": "2.0.10", "@types/punycode": "2.1.4", "@types/qrcode": "1.5.5", @@ -228,18 +227,17 @@ "@types/vary": "1.1.3", "@types/web-push": "3.6.3", "@types/ws": "8.5.10", - "@typescript-eslint/eslint-plugin": "7.7.1", - "@typescript-eslint/parser": "7.7.1", - "aws-sdk-client-mock": "3.0.1", + "@typescript-eslint/eslint-plugin": "7.15.0", + "@typescript-eslint/parser": "7.15.0", + "aws-sdk-client-mock": "4.0.1", "cross-env": "7.0.3", - "eslint": "8.57.0", "eslint-plugin-import": "2.29.1", - "execa": "8.0.1", - "fkill": "^9.0.0", + "execa": "9.2.0", + "fkill": "9.0.0", "jest": "29.7.0", "jest-mock": "29.7.0", - "nodemon": "3.1.0", + "nodemon": "3.1.4", "pid-port": "1.0.0", - "simple-oauth2": "5.0.0" + "simple-oauth2": "5.0.1" } } diff --git a/packages/backend/scripts/dev.mjs b/packages/backend/scripts/dev.mjs index 2d0de0f916..a3e0558abd 100644 --- a/packages/backend/scripts/dev.mjs +++ b/packages/backend/scripts/dev.mjs @@ -30,6 +30,7 @@ function execStart() { async function killProc() { if (backendProcess) { + backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す backendProcess.kill(); await new Promise(resolve => backendProcess.on('exit', resolve)); backendProcess = undefined; @@ -46,6 +47,7 @@ async function killProc() { ], { stdio: [process.stdin, process.stdout, process.stderr, 'ipc'], + serialization: "json", }) .on('message', async (message) => { if (message.type === 'exit') { diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 9786f8b8bb..74536c68f5 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -13,10 +13,12 @@ import { intersperse } from '@/misc/prelude/array.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; -import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js'; +import type { DefaultTreeAdapterMap } from 'parse5'; import type * as mfm from 'mfm-js'; -const treeAdapter = TreeAdapter.defaultTreeAdapter; +const treeAdapter = parse5.defaultTreeAdapter; +type Node = DefaultTreeAdapterMap['node']; +type ChildNode = DefaultTreeAdapterMap['childNode']; const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; @@ -46,7 +48,7 @@ export class MfmService { return text.trim(); - function getText(node: TreeAdapter.Node): string { + function getText(node: Node): string { if (treeAdapter.isTextNode(node)) return node.value; if (!treeAdapter.isElementNode(node)) return ''; if (node.nodeName === 'br') return '\n'; @@ -58,7 +60,7 @@ export class MfmService { return ''; } - function appendChildren(childNodes: TreeAdapter.ChildNode[]): void { + function appendChildren(childNodes: ChildNode[]): void { if (childNodes) { for (const n of childNodes) { analyze(n); @@ -66,14 +68,16 @@ export class MfmService { } } - function analyze(node: TreeAdapter.Node) { + function analyze(node: Node) { if (treeAdapter.isTextNode(node)) { text += node.value; return; } // Skip comment or document type node - if (!treeAdapter.isElementNode(node)) return; + if (!treeAdapter.isElementNode(node)) { + return; + } switch (node.nodeName) { case 'br': { @@ -81,8 +85,7 @@ export class MfmService { break; } - case 'a': - { + case 'a': { const txt = getText(node); const rel = node.attrs.find(x => x.name === 'rel'); const href = node.attrs.find(x => x.name === 'href'); @@ -90,7 +93,7 @@ export class MfmService { // ハッシュタグ if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) { text += txt; - // メンション + // メンション } else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) { const part = txt.split('@'); @@ -102,7 +105,7 @@ export class MfmService { } else if (part.length === 3) { text += txt; } - // その他 + // その他 } else { const generateLink = () => { if (!href && !txt) { @@ -130,8 +133,7 @@ export class MfmService { break; } - case 'h1': - { + case 'h1': { text += '【'; appendChildren(node.childNodes); text += '】\n'; @@ -139,16 +141,14 @@ export class MfmService { } case 'b': - case 'strong': - { + case 'strong': { text += '**'; appendChildren(node.childNodes); text += '**'; break; } - case 'small': - { + case 'small': { text += ''; appendChildren(node.childNodes); text += ''; @@ -156,8 +156,7 @@ export class MfmService { } case 's': - case 'del': - { + case 'del': { text += '~~'; appendChildren(node.childNodes); text += '~~'; @@ -165,8 +164,7 @@ export class MfmService { } case 'i': - case 'em': - { + case 'em': { text += ''; appendChildren(node.childNodes); text += ''; @@ -207,8 +205,7 @@ export class MfmService { case 'h3': case 'h4': case 'h5': - case 'h6': - { + case 'h6': { text += '\n\n'; appendChildren(node.childNodes); break; @@ -221,8 +218,7 @@ export class MfmService { case 'article': case 'li': case 'dt': - case 'dd': - { + case 'dd': { text += '\n'; appendChildren(node.childNodes); break; diff --git a/packages/backend/src/models/json-schema/drive-file.ts b/packages/backend/src/models/json-schema/drive-file.ts index ca88cc0e39..5ee1561c50 100644 --- a/packages/backend/src/models/json-schema/drive-file.ts +++ b/packages/backend/src/models/json-schema/drive-file.ts @@ -20,7 +20,7 @@ export const packedDriveFileSchema = { name: { type: 'string', optional: false, nullable: false, - example: 'lenna.jpg', + example: '192.jpg', }, type: { type: 'string', diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts index 459d8880fa..a7136d8c8c 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts @@ -61,7 +61,7 @@ export const meta = { name: { type: 'string', optional: false, nullable: false, - example: 'lenna.jpg', + example: '192.jpg', }, type: { type: 'string', diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index a8e702f328..b39b52bc41 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -257,7 +257,14 @@ export default class extends Endpoint { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (ps.name !== undefined) updates.name = ps.name; + if (ps.name !== undefined) { + if (ps.name === null) { + updates.name = null; + } else { + const trimmedName = ps.name.trim(); + updates.name = trimmedName === '' ? null : trimmedName; + } + } if (ps.description !== undefined) profileUpdates.description = ps.description; if (ps.lang !== undefined) profileUpdates.lang = ps.lang; if (ps.location !== undefined) profileUpdates.location = ps.location; diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 396536948e..4275dc9527 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -29,7 +29,8 @@ let forceError = localStorage.getItem('forceError'); if (forceError != null) { - renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.') + renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.'); + return; } //#region Detect language & fetch translations @@ -155,7 +156,12 @@ document.head.appendChild(css); } - function renderError(code, details) { + async function renderError(code, details) { + // Cannot set property 'innerHTML' of null を回避 + if (document.readyState === 'loading') { + await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); + } + let errorsElement = document.getElementById('errors'); if (!errorsElement) { @@ -314,6 +320,6 @@ #errorInfo { width: 50%; } - `) + }`) } })(); diff --git a/packages/backend/test-server/.eslintrc.cjs b/packages/backend/test-server/.eslintrc.cjs deleted file mode 100644 index c261741a36..0000000000 --- a/packages/backend/test-server/.eslintrc.cjs +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - }, - extends: [ - '../../shared/.eslintrc.js', - ], - rules: { - 'import/order': ['warn', { - 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], - 'pathGroups': [ - { - 'pattern': '@/**', - 'group': 'external', - 'position': 'after' - } - ], - }], - 'no-restricted-globals': [ - 'error', - { - 'name': '__dirname', - 'message': 'Not in ESModule. Use `import.meta.url` instead.' - }, - { - 'name': '__filename', - 'message': 'Not in ESModule. Use `import.meta.url` instead.' - } - ] - }, -}; diff --git a/packages/backend/test-server/eslint.config.js b/packages/backend/test-server/eslint.config.js new file mode 100644 index 0000000000..b9c16d469f --- /dev/null +++ b/packages/backend/test-server/eslint.config.js @@ -0,0 +1,43 @@ +import tsParser from '@typescript-eslint/parser'; +import sharedConfig from '../../shared/eslint.config.js'; + +export default [ + ...sharedConfig, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + 'import/order': ['warn', { + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'index', + 'object', + 'type', + ], + pathGroups: [{ + pattern: '@/**', + group: 'external', + position: 'after', + }], + }], + 'no-restricted-globals': ['error', { + name: '__dirname', + message: 'Not in ESModule. Use `import.meta.url` instead.', + }, { + name: '__filename', + message: 'Not in ESModule. Use `import.meta.url` instead.', + }], + }, + }, +]; diff --git a/packages/backend/test/.eslintrc.cjs b/packages/backend/test/.eslintrc.cjs deleted file mode 100644 index 41ecea0c3f..0000000000 --- a/packages/backend/test/.eslintrc.cjs +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - }, - extends: ['../.eslintrc.cjs'], - env: { - node: true, - jest: true, - }, -}; diff --git a/packages/backend/test/e2e/drive.ts b/packages/backend/test/e2e/drive.ts index 828c5200ef..43a73163eb 100644 --- a/packages/backend/test/e2e/drive.ts +++ b/packages/backend/test/e2e/drive.ts @@ -23,7 +23,7 @@ describe('Drive', () => { const marker = Math.random().toString(); - const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'; + const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg'; const catcher = makeStreamCatcher( alice, @@ -41,14 +41,14 @@ describe('Drive', () => { const file = await catcher; assert.strictEqual(res.status, 204); - assert.strictEqual(file.name, 'Lenna.jpg'); + assert.strictEqual(file.name, '192.jpg'); assert.strictEqual(file.type, 'image/jpeg'); }); test('ローカルからアップロードできる', async () => { // APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする - const res = await uploadFile(alice, { path: 'Lenna.jpg', name: 'テスト画像' }); + const res = await uploadFile(alice, { path: '192.jpg', name: 'テスト画像' }); assert.strictEqual(res.body?.name, 'テスト画像.jpg'); assert.strictEqual(res.body.type, 'image/jpeg'); diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index de5e8ba95e..2b2699ecd9 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -117,12 +117,21 @@ describe('Endpoints', () => { assert.strictEqual(res.body.birthday, myBirthday); }); - test('名前を空白にできる', async () => { + test('名前を空白のみにした場合nullになる', async () => { const res = await api('i/update', { name: ' ', }, alice); assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.name, ' '); + assert.strictEqual(res.body.name, null); + }); + + test('名前の前後に空白(ホワイトスペース)を入れてもトリムされる', async () => { + const res = await api('i/update', { + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#white_space + name: ' あ い う \u0009\u000b\u000c\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\ufeff', + }, alice); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.name, 'あ い う'); }); test('誕生日の設定を削除できる', async () => { @@ -584,7 +593,7 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body!.name, 'Lenna.jpg'); + assert.strictEqual(res.body!.name, '192.jpg'); }); test('ファイルに名前を付けられる', async () => { diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts index 74cf61a785..35240cd3c8 100644 --- a/packages/backend/test/e2e/move.ts +++ b/packages/backend/test/e2e/move.ts @@ -7,12 +7,13 @@ import { INestApplicationContext } from '@nestjs/common'; process.env.NODE_ENV = 'test'; +import { setTimeout } from 'node:timers/promises'; import * as assert from 'assert'; import { loadConfig } from '@/config.js'; import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { jobQueue } from '@/boot/common.js'; -import { api, initTestDb, signup, sleep, successfulApiCall, uploadFile } from '../utils.js'; +import { api, initTestDb, signup, successfulApiCall, uploadFile } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Account Move', () => { @@ -271,7 +272,7 @@ describe('Account Move', () => { assert.strictEqual(move.status, 200); - await sleep(1000 * 3); // wait for jobs to finish + await setTimeout(1000 * 3); // wait for jobs to finish // Unfollow delayed? const aliceFollowings = await api('users/following', { @@ -330,7 +331,7 @@ describe('Account Move', () => { }); test('Unfollowed after 10 sec (24 hours in production).', async () => { - await sleep(1000 * 8); + await setTimeout(1000 * 8); const following = await api('users/following', { userId: alice.id, diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index bda31d9640..7ce9f47bc3 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -41,7 +41,7 @@ describe('Note', () => { }); test('ファイルを添付できる', async () => { - const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); + const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg'); const res = await api('notes/create', { fileIds: [file.id], @@ -53,7 +53,7 @@ describe('Note', () => { }, 1000 * 10); test('他人のファイルで怒られる', async () => { - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg'); const res = await api('notes/create', { text: 'test', diff --git a/packages/backend/test/e2e/renote-mute.ts b/packages/backend/test/e2e/renote-mute.ts index 1abbb4f044..f6895c43d8 100644 --- a/packages/backend/test/e2e/renote-mute.ts +++ b/packages/backend/test/e2e/renote-mute.ts @@ -6,7 +6,8 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { api, post, signup, sleep, waitFire } from '../utils.js'; +import { setTimeout } from 'node:timers/promises'; +import { api, post, signup, waitFire } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Renote Mute', () => { @@ -35,7 +36,7 @@ describe('Renote Mute', () => { const carolNote = await post(carol, { text: 'hi' }); // redisに追加されるのを待つ - await sleep(100); + await setTimeout(100); const res = await api('notes/local-timeline', {}, alice); @@ -52,7 +53,7 @@ describe('Renote Mute', () => { const carolNote = await post(carol, { text: 'hi' }); // redisに追加されるのを待つ - await sleep(100); + await setTimeout(100); const res = await api('notes/local-timeline', {}, alice); @@ -69,7 +70,7 @@ describe('Renote Mute', () => { const bobRenote = await post(bob, { renoteId: carolNote.id }); // redisに追加されるのを待つ - await sleep(100); + await setTimeout(100); const res = await api('notes/local-timeline', {}, alice); diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index f6cc2bac28..fccc052d99 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -7,16 +7,17 @@ // pnpm jest -- e2e/timelines.ts import * as assert from 'assert'; +import { setTimeout } from 'node:timers/promises'; import { Redis } from 'ioredis'; import { loadConfig } from '@/config.js'; -import { api, post, randomString, sendEnvUpdateRequest, signup, sleep, uploadUrl } from '../utils.js'; +import { api, post, randomString, sendEnvUpdateRequest, signup, uploadUrl } from '../utils.js'; function genHost() { return randomString() + '.example.com'; } function waitForPushToTl() { - return sleep(500); + return setTimeout(500); } let redisForTimelines: Redis; @@ -44,7 +45,7 @@ describe('Timelines', () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' }); @@ -60,7 +61,7 @@ describe('Timelines', () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); const carolNote = await post(carol, { text: 'hi' }); @@ -77,7 +78,7 @@ describe('Timelines', () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -94,7 +95,7 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -111,7 +112,7 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); @@ -128,7 +129,7 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -147,7 +148,7 @@ describe('Timelines', () => { await api('following/create', { userId: carol.id }, alice); await api('following/create', { userId: carol.id }, bob); await api('following/update', { userId: bob.id, withReplies: true }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -166,7 +167,7 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: carol.id }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); @@ -182,7 +183,7 @@ describe('Timelines', () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); @@ -198,7 +199,7 @@ describe('Timelines', () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); @@ -228,7 +229,7 @@ describe('Timelines', () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { renoteId: carolNote.id }); @@ -244,7 +245,7 @@ describe('Timelines', () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { renoteId: carolNote.id }); @@ -262,7 +263,7 @@ describe('Timelines', () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); @@ -280,7 +281,7 @@ describe('Timelines', () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); await waitForPushToTl(); @@ -295,7 +296,7 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('mute/create', { userId: carol.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); @@ -313,7 +314,7 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('mute/create', { userId: carol.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -359,7 +360,7 @@ describe('Timelines', () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const [bobFile, carolFile] = await Promise.all([ uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), @@ -384,7 +385,7 @@ describe('Timelines', () => { const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); await waitForPushToTl(); @@ -411,7 +412,7 @@ describe('Timelines', () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); await waitForPushToTl(); @@ -438,7 +439,7 @@ describe('Timelines', () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); await waitForPushToTl(); @@ -566,7 +567,7 @@ describe('Timelines', () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: carol.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi', visibility: 'home' }); const bobNote = await post(bob, { text: 'hi' }); @@ -582,7 +583,7 @@ describe('Timelines', () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('mute/create', { userId: carol.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi' }); @@ -599,7 +600,7 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('mute/create', { userId: carol.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); @@ -617,7 +618,7 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('mute/create', { userId: carol.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -633,7 +634,7 @@ describe('Timelines', () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); @@ -703,7 +704,7 @@ describe('Timelines', () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); await waitForPushToTl(); @@ -717,7 +718,7 @@ describe('Timelines', () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); @@ -820,7 +821,7 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi' }); await waitForPushToTl(); @@ -835,7 +836,7 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); await waitForPushToTl(); @@ -850,7 +851,7 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); await waitForPushToTl(); @@ -865,7 +866,7 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -881,7 +882,7 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); @@ -899,7 +900,7 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); - await sleep(1000); + await setTimeout(1000); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); @@ -916,7 +917,7 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -933,7 +934,7 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -950,7 +951,7 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); await waitForPushToTl(); @@ -966,7 +967,7 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); await waitForPushToTl(); @@ -982,7 +983,7 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: alice.id }, alice); - await sleep(1000); + await setTimeout(1000); const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); await waitForPushToTl(); @@ -999,7 +1000,7 @@ describe('Timelines', () => { const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); await waitForPushToTl(); @@ -1031,7 +1032,7 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); await waitForPushToTl(); @@ -1048,7 +1049,7 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: carol.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); await waitForPushToTl(); @@ -1088,7 +1089,7 @@ describe('Timelines', () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); await waitForPushToTl(); @@ -1228,7 +1229,7 @@ describe('Timelines', () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('mute/create', { userId: carol.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); @@ -1243,7 +1244,7 @@ describe('Timelines', () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('mute/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id }); diff --git a/packages/backend/test/e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts index 331e053935..cc07c5ae71 100644 --- a/packages/backend/test/e2e/user-notes.ts +++ b/packages/backend/test/e2e/user-notes.ts @@ -17,8 +17,8 @@ describe('users/notes', () => { beforeAll(async () => { alice = await signup({ username: 'alice' }); - const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); - const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png'); + const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg'); + const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.png'); jpgNote = await post(alice, { fileIds: [jpg.id], }); diff --git a/packages/backend/test/eslint.config.js b/packages/backend/test/eslint.config.js new file mode 100644 index 0000000000..a0f43babad --- /dev/null +++ b/packages/backend/test/eslint.config.js @@ -0,0 +1,22 @@ +import globals from 'globals'; +import tsParser from '@typescript-eslint/parser'; +import sharedConfig from '../../shared/eslint.config.js'; + +export default [ + ...sharedConfig, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + parserOptions: { + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; diff --git a/packages/backend/test/resources/192.jpg b/packages/backend/test/resources/192.jpg new file mode 100644 index 0000000000..76374628e0 Binary files /dev/null and b/packages/backend/test/resources/192.jpg differ diff --git a/packages/backend/test/resources/192.png b/packages/backend/test/resources/192.png new file mode 100644 index 0000000000..15fd1e3731 Binary files /dev/null and b/packages/backend/test/resources/192.png differ diff --git a/packages/backend/test/resources/Lenna.jpg b/packages/backend/test/resources/Lenna.jpg deleted file mode 100644 index 6b5b32281c..0000000000 Binary files a/packages/backend/test/resources/Lenna.jpg and /dev/null differ diff --git a/packages/backend/test/resources/Lenna.png b/packages/backend/test/resources/Lenna.png deleted file mode 100644 index 59ef68aabd..0000000000 Binary files a/packages/backend/test/resources/Lenna.png and /dev/null differ diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts index 40d187f5a8..aa9b34b706 100644 --- a/packages/backend/test/unit/FileInfoService.ts +++ b/packages/backend/test/unit/FileInfoService.ts @@ -83,21 +83,21 @@ describe('FileInfoService', () => { describe('IMAGE', () => { test('Generic JPEG', async () => { - const path = `${resources}/Lenna.jpg`; + const path = `${resources}/192.jpg`; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; delete info.warnings; delete info.blurhash; delete info.sensitive; delete info.porn; assert.deepStrictEqual(info, { - size: 25360, - md5: '091b3f259662aa31e2ffef4519951168', + size: 5131, + md5: '8c9ed0677dd2b8f9f7472c3af247e5e3', type: { mime: 'image/jpeg', ext: 'jpg', }, - width: 512, - height: 512, + width: 192, + height: 192, orientation: undefined, }); }); diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index 69fa4162fb..b6cbe4c520 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -5,6 +5,7 @@ process.env.NODE_ENV = 'test'; +import { setTimeout } from 'node:timers/promises'; import { jest } from '@jest/globals'; import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; @@ -29,7 +30,6 @@ import { secureRndstr } from '@/misc/secure-rndstr.js'; import { NotificationService } from '@/core/NotificationService.js'; import { RoleCondFormulaValue } from '@/models/Role.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { sleep } from '../utils.js'; import type { TestingModule } from '@nestjs/testing'; import type { MockFunctionMetadata } from 'jest-mock'; @@ -278,7 +278,7 @@ describe('RoleService', () => { // ストリーミング経由で反映されるまでちょっと待つ clock.uninstall(); - await sleep(100); + await setTimeout(100); const resultAfter25hAgain = await roleService.getUserPolicies(user.id); expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true); @@ -807,7 +807,7 @@ describe('RoleService', () => { await roleService.assign(user.id, role.id); clock.uninstall(); - await sleep(100); + await setTimeout(100); const assignments = await roleAssignmentsRepository.find({ where: { @@ -835,7 +835,7 @@ describe('RoleService', () => { await roleService.assign(user.id, role.id); clock.uninstall(); - await sleep(100); + await setTimeout(100); const assignments = await roleAssignmentsRepository.find({ where: { diff --git a/packages/backend/test/unit/SystemWebhookService.ts b/packages/backend/test/unit/SystemWebhookService.ts index 41b7f977ca..790cd1490e 100644 --- a/packages/backend/test/unit/SystemWebhookService.ts +++ b/packages/backend/test/unit/SystemWebhookService.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { setTimeout } from 'node:timers/promises'; import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals'; import { Test, TestingModule } from '@nestjs/testing'; import { MiUser } from '@/models/User.js'; @@ -16,7 +17,7 @@ import { DI } from '@/di-symbols.js'; import { QueueService } from '@/core/QueueService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js'; -import { randomString, sleep } from '../utils.js'; +import { randomString } from '../utils.js'; describe('SystemWebhookService', () => { let app: TestingModule; @@ -358,7 +359,7 @@ describe('SystemWebhookService', () => { ); // redisでの配信経由で更新されるのでちょっと待つ - await sleep(500); + await setTimeout(500); const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); expect(fetchedWebhooks).toEqual([webhook]); @@ -377,7 +378,7 @@ describe('SystemWebhookService', () => { ); // redisでの配信経由で更新されるのでちょっと待つ - await sleep(500); + await setTimeout(500); const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); expect(fetchedWebhooks).toEqual([]); @@ -407,7 +408,7 @@ describe('SystemWebhookService', () => { ); // redisでの配信経由で更新されるのでちょっと待つ - await sleep(500); + await setTimeout(500); const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); expect(fetchedWebhooks).toEqual([webhook2]); @@ -434,7 +435,7 @@ describe('SystemWebhookService', () => { ); // redisでの配信経由で更新されるのでちょっと待つ - await sleep(500); + await setTimeout(500); const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); expect(fetchedWebhooks.length).toEqual(0); @@ -457,7 +458,7 @@ describe('SystemWebhookService', () => { ); // redisでの配信経由で更新されるのでちょっと待つ - await sleep(500); + await setTimeout(500); const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); expect(fetchedWebhooks).toEqual([webhook2]); @@ -481,7 +482,7 @@ describe('SystemWebhookService', () => { ); // redisでの配信経由で更新されるのでちょっと待つ - await sleep(500); + await setTimeout(500); const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); expect(fetchedWebhooks.length).toEqual(0); @@ -504,7 +505,7 @@ describe('SystemWebhookService', () => { ); // redisでの配信経由で更新されるのでちょっと待つ - await sleep(500); + await setTimeout(500); const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); expect(fetchedWebhooks.length).toEqual(0); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index aad4ab37c9..06c3f82601 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -297,7 +297,7 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO body: misskey.entities.DriveFile | null }> => { const absPath = path == null - ? new URL('resources/Lenna.jpg', import.meta.url) + ? new URL('resources/192.jpg', import.meta.url) : isAbsolute(path.toString()) ? new URL(path) : new URL(path, new URL('resources/', import.meta.url)); @@ -605,14 +605,6 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) { return db; } -export function sleep(msec: number) { - return new Promise(res => { - setTimeout(() => { - res(); - }, msec); - }); -} - export async function sendEnvUpdateRequest(params: { key: string, value?: string }) { const res = await fetch( `http://localhost:${port + 1000}/env`, diff --git a/packages/frontend/.eslintrc.cjs b/packages/frontend/.eslintrc.cjs deleted file mode 100644 index 50a600dfa8..0000000000 --- a/packages/frontend/.eslintrc.cjs +++ /dev/null @@ -1,81 +0,0 @@ -module.exports = { - root: true, - env: { - 'node': false, - }, - parser: 'vue-eslint-parser', - parserOptions: { - 'parser': '@typescript-eslint/parser', - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - extraFileExtensions: ['.vue'], - }, - extends: [ - '../shared/.eslintrc.js', - 'plugin:vue/vue3-recommended', - ], - rules: { - '@typescript-eslint/no-empty-interface': [ - 'error', - { - 'allowSingleExtends': true, - }, - ], - // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため - 'id-denylist': ['error', 'window'], - 'no-shadow': ['warn'], - 'vue/attributes-order': ['error', { - 'alphabetical': false, - }], - 'vue/no-use-v-if-with-v-for': ['error', { - 'allowUsingIterationVar': false, - }], - 'vue/no-ref-as-operand': 'error', - 'vue/no-multi-spaces': ['error', { - 'ignoreProperties': false, - }], - 'vue/no-v-html': 'warn', - 'vue/order-in-components': 'error', - 'vue/html-indent': ['warn', 'tab', { - 'attribute': 1, - 'baseIndent': 0, - 'closeBracket': 0, - 'alignAttributesVertically': true, - 'ignores': [], - }], - 'vue/html-closing-bracket-spacing': ['warn', { - 'startTag': 'never', - 'endTag': 'never', - 'selfClosingTag': 'never', - }], - 'vue/multi-word-component-names': 'warn', - 'vue/require-v-for-key': 'warn', - 'vue/no-unused-components': 'warn', - 'vue/no-unused-vars': 'warn', - 'vue/no-dupe-keys': 'warn', - 'vue/valid-v-for': 'warn', - 'vue/return-in-computed-property': 'warn', - 'vue/no-setup-props-reactivity-loss': 'warn', - 'vue/max-attributes-per-line': 'off', - 'vue/html-self-closing': 'off', - 'vue/singleline-html-element-content-newline': 'off', - 'vue/v-on-event-hyphenation': ['error', 'never', { autofix: true }], - 'vue/attribute-hyphenation': ['error', 'never'], - }, - globals: { - // Node.js - 'module': false, - 'require': false, - '__dirname': false, - - // Misskey - '_DEV_': false, - '_LANGS_': false, - '_VERSION_': false, - '_ENV_': false, - '_PERF_PREFIX_': false, - '_DATA_TRANSFER_DRIVE_FILE_': false, - '_DATA_TRANSFER_DRIVE_FOLDER_': false, - '_DATA_TRANSFER_DECK_COLUMN_': false, - }, -}; diff --git a/packages/frontend/.storybook/changes.ts b/packages/frontend/.storybook/changes.ts index 7c70972e1e..bc7601441c 100644 --- a/packages/frontend/.storybook/changes.ts +++ b/packages/frontend/.storybook/changes.ts @@ -47,7 +47,6 @@ await fs.readFile( ) ) .map((path) => path.replace(/(?:(?<=\.stories)\.(?:impl|meta)|\.msw)(?=\.ts$)/g, '')) - .map((path) => (path.startsWith('.') ? path : `./${path}`)) ); if ( micromatch(Array.from(modules), [ diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts index 73ee007fb8..d000a28232 100644 --- a/packages/frontend/.storybook/preview.ts +++ b/packages/frontend/.storybook/preview.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { FORCE_REMOUNT } from '@storybook/core-events'; +import { FORCE_RE_RENDER, FORCE_REMOUNT } from '@storybook/core-events'; import { addons } from '@storybook/preview-api'; import { type Preview, setup } from '@storybook/vue3'; import isChromatic from 'chromatic/isChromatic'; @@ -16,7 +16,7 @@ import '../src/style.scss'; const appInitialized = Symbol(); -let lastStory = null; +let lastStory: string | null = null; let moduleInitialized = false; let unobserve = () => {}; let misskeyOS = null; @@ -110,7 +110,7 @@ const preview = { }).catch(() => {}); Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => { initLocalStorage(); - channel.emit(FORCE_REMOUNT, { storyId: context.id }); + channel.emit(FORCE_RE_RENDER, { storyId: context.id }); }); } const story = Story(); diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js new file mode 100644 index 0000000000..dd8f03dac5 --- /dev/null +++ b/packages/frontend/eslint.config.js @@ -0,0 +1,95 @@ +import globals from 'globals'; +import tsParser from '@typescript-eslint/parser'; +import parser from 'vue-eslint-parser'; +import pluginVue from 'eslint-plugin-vue'; +import pluginMisskey from '@misskey-dev/eslint-plugin'; +import sharedConfig from '../shared/eslint.config.js'; + +export default [ + ...sharedConfig, + { + files: ['src/**/*.vue'], + ...pluginMisskey.configs.typescript, + }, + ...pluginVue.configs['flat/recommended'], + { + files: ['src/**/*.{ts,vue}'], + languageOptions: { + globals: { + ...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])), + ...globals.browser, + + // Node.js + module: false, + require: false, + __dirname: false, + + // Misskey + _DEV_: false, + _LANGS_: false, + _VERSION_: false, + _ENV_: false, + _PERF_PREFIX_: false, + _DATA_TRANSFER_DRIVE_FILE_: false, + _DATA_TRANSFER_DRIVE_FOLDER_: false, + _DATA_TRANSFER_DECK_COLUMN_: false, + }, + parser, + parserOptions: { + extraFileExtensions: ['.vue'], + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + '@typescript-eslint/no-empty-interface': ['error', { + allowSingleExtends: true, + }], + // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため + // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため + 'id-denylist': ['error', 'window', 'e'], + 'no-shadow': ['warn'], + 'vue/attributes-order': ['error', { + alphabetical: false, + }], + 'vue/no-use-v-if-with-v-for': ['error', { + allowUsingIterationVar: false, + }], + 'vue/no-ref-as-operand': 'error', + 'vue/no-multi-spaces': ['error', { + ignoreProperties: false, + }], + 'vue/no-v-html': 'warn', + 'vue/order-in-components': 'error', + 'vue/html-indent': ['warn', 'tab', { + attribute: 1, + baseIndent: 0, + closeBracket: 0, + alignAttributesVertically: true, + ignores: [], + }], + 'vue/html-closing-bracket-spacing': ['warn', { + startTag: 'never', + endTag: 'never', + selfClosingTag: 'never', + }], + 'vue/multi-word-component-names': 'warn', + 'vue/require-v-for-key': 'warn', + 'vue/no-unused-components': 'warn', + 'vue/no-unused-vars': 'warn', + 'vue/no-dupe-keys': 'warn', + 'vue/valid-v-for': 'warn', + 'vue/return-in-computed-property': 'warn', + 'vue/no-setup-props-reactivity-loss': 'warn', + 'vue/max-attributes-per-line': 'off', + 'vue/html-self-closing': 'off', + 'vue/singleline-html-element-content-newline': 'off', + 'vue/v-on-event-hyphenation': ['error', 'never', { + autofix: true, + }], + 'vue/attribute-hyphenation': ['error', 'never'], + }, + }, +]; diff --git a/packages/frontend/package.json b/packages/frontend/package.json index c428139e52..9755032a7e 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -22,24 +22,24 @@ "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@misskey-dev/browser-image-resizer": "2024.1.0", "@rollup/plugin-json": "6.1.0", - "@rollup/plugin-replace": "5.0.5", + "@rollup/plugin-replace": "5.0.7", "@rollup/pluginutils": "5.1.0", "@syuilo/aiscript": "0.18.0", "@tabler/icons-webfont": "3.3.0", "@twemoji/parser": "15.1.1", - "@vitejs/plugin-vue": "5.0.4", - "@vue/compiler-sfc": "3.4.26", + "@vitejs/plugin-vue": "5.0.5", + "@vue/compiler-sfc": "3.4.31", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.9", "astring": "1.8.6", "broadcast-channel": "7.0.0", "buraha": "0.0.1", "canvas-confetti": "1.9.3", - "chart.js": "4.4.2", + "chart.js": "4.4.3", "chartjs-adapter-date-fns": "3.0.0", "chartjs-chart-matrix": "2.0.1", "chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-zoom": "2.0.1", - "chromatic": "11.3.0", + "chromatic": "11.5.4", "compare-versions": "6.1.0", "cropperjs": "2.0.0-beta.5", "date-fns": "2.30.0", @@ -56,89 +56,87 @@ "misskey-mahjong": "workspace:*", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", - "photoswipe": "5.4.3", + "photoswipe": "5.4.4", "punycode": "2.3.1", - "rollup": "4.17.2", + "rollup": "4.18.0", "sanitize-html": "2.13.0", - "sass": "1.76.0", - "shiki": "1.4.0", + "sass": "1.77.6", + "shiki": "1.10.0", "strict-event-emitter-types": "2.0.0", "textarea-caret": "3.1.0", - "three": "0.164.1", - "throttle-debounce": "5.0.0", + "three": "0.165.0", + "throttle-debounce": "5.0.2", "tinycolor2": "1.6.0", - "tsc-alias": "1.8.8", + "tsc-alias": "1.8.10", "tsconfig-paths": "4.2.0", - "typescript": "5.5.2", - "uuid": "9.0.1", - "v-code-diff": "1.11.0", - "vite": "5.2.11", - "vue": "3.4.26", + "typescript": "5.5.3", + "uuid": "10.0.0", + "v-code-diff": "1.12.0", + "vite": "5.3.2", + "vue": "3.4.31", "vuedraggable": "next" }, "devDependencies": { - "@misskey-dev/eslint-plugin": "1.0.0", "@misskey-dev/summaly": "5.1.0", - "@storybook/addon-actions": "8.0.9", - "@storybook/addon-essentials": "8.0.9", - "@storybook/addon-interactions": "8.0.9", - "@storybook/addon-links": "8.0.9", - "@storybook/addon-mdx-gfm": "8.0.9", - "@storybook/addon-storysource": "8.0.9", - "@storybook/blocks": "8.0.9", - "@storybook/components": "8.0.9", - "@storybook/core-events": "8.0.9", - "@storybook/manager-api": "8.0.9", - "@storybook/preview-api": "8.0.9", - "@storybook/react": "8.0.9", - "@storybook/react-vite": "8.0.9", - "@storybook/test": "8.0.9", - "@storybook/theming": "8.0.9", - "@storybook/types": "8.0.9", - "@storybook/vue3": "8.0.9", - "@storybook/vue3-vite": "8.0.9", - "@testing-library/vue": "8.0.3", + "@storybook/addon-actions": "8.1.11", + "@storybook/addon-essentials": "8.1.11", + "@storybook/addon-interactions": "8.1.11", + "@storybook/addon-links": "8.1.11", + "@storybook/addon-mdx-gfm": "8.1.11", + "@storybook/addon-storysource": "8.1.11", + "@storybook/blocks": "8.1.11", + "@storybook/components": "8.1.11", + "@storybook/core-events": "8.1.11", + "@storybook/manager-api": "8.1.11", + "@storybook/preview-api": "8.1.11", + "@storybook/react": "8.1.11", + "@storybook/react-vite": "8.1.11", + "@storybook/test": "8.1.11", + "@storybook/theming": "8.1.11", + "@storybook/types": "8.1.11", + "@storybook/vue3": "8.1.11", + "@storybook/vue3-vite": "8.1.11", + "@testing-library/vue": "8.1.0", "@types/escape-regexp": "0.0.3", "@types/estree": "1.0.5", "@types/matter-js": "0.19.6", - "@types/micromatch": "4.0.7", - "@types/node": "20.12.7", + "@types/micromatch": "4.0.9", + "@types/node": "20.14.9", "@types/punycode": "2.1.4", "@types/sanitize-html": "2.11.0", "@types/seedrandom": "3.0.8", "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", - "@types/uuid": "9.0.8", + "@types/uuid": "10.0.0", "@types/ws": "8.5.10", - "@typescript-eslint/eslint-plugin": "7.7.1", - "@typescript-eslint/parser": "7.7.1", - "@vitest/coverage-v8": "0.34.6", - "@vue/runtime-core": "3.4.26", - "acorn": "8.11.3", + "@typescript-eslint/eslint-plugin": "7.15.0", + "@typescript-eslint/parser": "7.15.0", + "@vitest/coverage-v8": "1.6.0", + "@vue/runtime-core": "3.4.31", + "acorn": "8.12.0", "cross-env": "7.0.3", - "cypress": "13.8.1", - "eslint": "8.57.0", + "cypress": "13.13.0", "eslint-plugin-import": "2.29.1", - "eslint-plugin-vue": "9.25.0", + "eslint-plugin-vue": "9.26.0", "fast-glob": "3.3.2", "happy-dom": "10.0.3", "intersection-observer": "0.12.2", - "micromatch": "4.0.5", - "msw": "2.2.14", - "msw-storybook-addon": "2.0.1", - "nodemon": "3.1.0", - "prettier": "3.2.5", + "micromatch": "4.0.7", + "msw": "2.3.1", + "msw-storybook-addon": "2.0.2", + "nodemon": "3.1.4", + "prettier": "3.3.2", "react": "18.3.1", "react-dom": "18.3.1", "seedrandom": "3.0.5", - "start-server-and-test": "2.0.3", - "storybook": "8.0.9", + "start-server-and-test": "2.0.4", + "storybook": "8.1.11", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "vite-plugin-turbosnap": "1.0.3", - "vitest": "0.34.6", + "vitest": "1.6.0", "vitest-fetch-mock": "0.2.2", - "vue-component-type-helpers": "2.0.16", - "vue-eslint-parser": "9.4.2", - "vue-tsc": "2.0.16" + "vue-component-type-helpers": "2.0.24", + "vue-eslint-parser": "9.4.3", + "vue-tsc": "2.0.24" } } diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index f99b550a83..4172016f89 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -184,10 +184,12 @@ export async function refreshAccount() { export async function login(token: Account['token'], redirect?: string) { const showing = ref(true); - popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), { success: false, showing: showing, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); if (_DEV_) console.log('logging as token ', token); const me = await fetchAccount(token, undefined, true) .catch(reason => { @@ -223,21 +225,23 @@ export async function openAccountMenu(opts: { if (!$i) return; function showSigninDialog() { - popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, { done: res => { addAccount(res.id, res.i); success(); }, - }, 'closed'); + closed: () => dispose(), + }); } function createAccount() { - popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, { done: res => { addAccount(res.id, res.i); switchAccountWithToken(res.i); }, - }, 'closed'); + closed: () => dispose(), + }); } async function switchAccount(account: Misskey.entities.UserDetailed) { diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 5cb19f388a..faf230a1a2 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -35,7 +35,9 @@ export async function mainBoot() { emojiPicker.init(); if (isClientUpdated && $i) { - popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed'); + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, { + closed: () => dispose(), + }); } const stream = useStream(); @@ -96,7 +98,7 @@ export async function mainBoot() { }).render(); } } - } + } } catch (error) { // console.error(error); console.error('Failed to initialise the seasonal screen effect canvas context:', error); @@ -108,22 +110,28 @@ export async function mainBoot() { defaultStore.loaded.then(() => { if (defaultStore.state.accountSetupWizard !== -1) { - popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed'); + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, { + closed: () => dispose(), + }); } }); for (const announcement of ($i.unreadAnnouncements ?? []).filter(x => x.display === 'dialog')) { - popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), { announcement, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); } stream.on('announcementCreated', (ev) => { const announcement = ev.announcement; if (announcement.display === 'dialog') { - popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), { announcement, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); } }); @@ -247,13 +255,17 @@ export async function mainBoot() { const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo'); if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) { if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) { - popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed'); + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, { + closed: () => dispose(), + }); } } const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read'); if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') { - popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed'); + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, { + closed: () => dispose(), + }); } if ('Notification' in window) { diff --git a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts index b99620da22..b9770670dc 100644 --- a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts +++ b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts @@ -12,14 +12,12 @@ import { expect, userEvent, within } from '@storybook/test'; import { channel } from '../../.storybook/fakes.js'; import { commonHandlers } from '../../.storybook/mocks.js'; import MkChannelFollowButton from './MkChannelFollowButton.vue'; -import { semaphore } from '@/scripts/test-utils.js'; import { i18n } from '@/i18n.js'; function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } -const s = semaphore(); export const Default = { render(args) { return { @@ -46,17 +44,13 @@ export const Default = { full: true, }, async play({ canvasElement }) { - await s.acquire(); - await sleep(1000); const canvas = within(canvasElement); const buttonElement = canvas.getByRole('button'); await expect(buttonElement).toHaveTextContent(i18n.ts.follow); await userEvent.click(buttonElement); await sleep(1000); await expect(buttonElement).toHaveTextContent(i18n.ts.unfollow); - await sleep(100); await userEvent.click(buttonElement); - s.release(); }, parameters: { layout: 'centered', diff --git a/packages/frontend/src/components/MkClickerGame.stories.impl.ts b/packages/frontend/src/components/MkClickerGame.stories.impl.ts index 8378010f8b..36313f965d 100644 --- a/packages/frontend/src/components/MkClickerGame.stories.impl.ts +++ b/packages/frontend/src/components/MkClickerGame.stories.impl.ts @@ -8,7 +8,7 @@ import { StoryObj } from '@storybook/vue3'; import { HttpResponse, http } from 'msw'; import { action } from '@storybook/addon-actions'; -import { expect, within } from '@storybook/test'; +import { expect, userEvent, within } from '@storybook/test'; import { commonHandlers } from '../../.storybook/mocks.js'; import MkClickerGame from './MkClickerGame.vue'; @@ -41,12 +41,10 @@ export const Default = { await sleep(1000); const canvas = within(canvasElement); const count = canvas.getByTestId('count'); - // NOTE: flaky なので N/A も通しておく - await expect(count).toHaveTextContent(/^(0|N\/A)$/); - // FIXME: flaky - // const buttonElement = canvas.getByRole('button'); - // await userEvent.click(buttonElement); - // await expect(count).toHaveTextContent('1'); + await expect(count).toHaveTextContent('0'); + const buttonElement = canvas.getByRole('button'); + await userEvent.click(buttonElement); + await expect(count).toHaveTextContent('1'); }, parameters: { layout: 'centered', diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue index b592609e18..00506fb735 100644 --- a/packages/frontend/src/components/MkClickerGame.vue +++ b/packages/frontend/src/components/MkClickerGame.vue @@ -35,7 +35,9 @@ const prevCookies = ref(0); function onClick(ev: MouseEvent) { const x = ev.clientX; const y = ev.clientY; - os.popup(MkPlusOneEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkPlusOneEffect, { x, y }, { + end: () => dispose(), + }); saveData.value!.cookies++; saveData.value!.totalCookies++; diff --git a/packages/frontend/src/components/MkCwButton.stories.impl.ts b/packages/frontend/src/components/MkCwButton.stories.impl.ts index 05c6001552..5d6ea56da9 100644 --- a/packages/frontend/src/components/MkCwButton.stories.impl.ts +++ b/packages/frontend/src/components/MkCwButton.stories.impl.ts @@ -11,13 +11,6 @@ import { expect, userEvent, within } from '@storybook/test'; import { file } from '../../.storybook/fakes.js'; import MkCwButton from './MkCwButton.vue'; import { i18n } from '@/i18n.js'; -import { semaphore } from '@/scripts/test-utils.js'; - -function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -const s = semaphore(); export const Default = { render(args) { @@ -54,8 +47,6 @@ export const Default = { text: 'Some CW content', }, async play({ canvasElement }) { - await s.acquire(); - await sleep(1000); const canvas = within(canvasElement); const buttonElement = canvas.getByRole('button'); await expect(buttonElement).toHaveTextContent(i18n.ts._cw.show); @@ -63,7 +54,6 @@ export const Default = { await userEvent.click(buttonElement); await expect(buttonElement).toHaveTextContent(i18n.ts._cw.hide); await userEvent.click(buttonElement); - s.release(); }, parameters: { chromatic: { diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 8da0d78f35..1cc8b15b73 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -257,10 +257,11 @@ function onContextmenu(ev: MouseEvent) { text: i18n.ts.openInWindow, icon: 'ti ti-app-window', action: () => { - os.popup(defineAsyncComponent(() => import('@/components/MkDriveWindow.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkDriveWindow.vue')), { initialFolder: props.folder, }, { - }, 'closed'); + closed: () => dispose(), + }); }, }, { type: 'divider' }, { text: i18n.ts.rename, diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 8a6bef54d8..4bd4bee1e5 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -402,7 +402,9 @@ function chosen(emoji: any, ev?: MouseEvent) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } const key = getKey(emoji); diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue index 5d54a58e97..e842ec2d6e 100644 --- a/packages/frontend/src/components/MkLink.vue +++ b/packages/frontend/src/components/MkLink.vue @@ -37,11 +37,13 @@ const el = ref(); if (isEnabledUrlPreview.value) { useTooltip(el, (showing) => { - os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), { showing, url: props.url, source: el.value instanceof HTMLElement ? el.value : el.value?.$el, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); } diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 22b1691a86..1313e4c58e 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -335,12 +335,14 @@ if (!props.mock) { if (users.length < 1) return; - os.popup(MkUsersTooltip, { + const { dispose } = os.popup(MkUsersTooltip, { showing, users, count: appearNote.value.renoteCount, targetElement: renoteButton.value, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); if (appearNote.value.reactionAcceptance === 'likeOnly') { @@ -355,13 +357,15 @@ if (!props.mock) { if (users.length < 1) return; - os.popup(MkReactionsViewerDetails, { + const { dispose } = os.popup(MkReactionsViewerDetails, { showing, reaction: '❤️', users, count: appearNote.value.reactionCount, targetElement: reactButton.value!, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); } } @@ -409,7 +413,9 @@ function react(viaKeyboard = false): void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } else { blur(); diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index ed1c0a9e96..bc1f416373 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -346,12 +346,14 @@ useTooltip(renoteButton, async (showing) => { if (users.length < 1) return; - os.popup(MkUsersTooltip, { + const { dispose } = os.popup(MkUsersTooltip, { showing, users, count: appearNote.value.renoteCount, targetElement: renoteButton.value, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); if (appearNote.value.reactionAcceptance === 'likeOnly') { @@ -366,13 +368,15 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') { if (users.length < 1) return; - os.popup(MkReactionsViewerDetails, { + const { dispose } = os.popup(MkReactionsViewerDetails, { showing, reaction: '❤️', users, count: appearNote.value.reactionCount, targetElement: reactButton.value!, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); } @@ -413,7 +417,9 @@ function react(viaKeyboard = false): void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } else { blur(); diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 1df9007681..0dc1aa0891 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -463,7 +463,7 @@ function setVisibility() { return; } - os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), { currentVisibility: visibility.value, isSilenced: $i.isSilenced, localOnly: localOnly.value, @@ -476,7 +476,8 @@ function setVisibility() { defaultStore.set('visibility', visibility.value); } }, - }, 'closed'); + closed: () => dispose(), + }); } async function toggleLocalOnly() { @@ -624,8 +625,8 @@ async function onPaste(ev: ClipboardEvent) { return; } - const fileName = formatTimeString(new Date(), defaultStore.state.pastedFileName).replace(/{{number}}/g, "0"); - const file = new File([paste], `${fileName}.txt`, { type: "text/plain" }); + const fileName = formatTimeString(new Date(), defaultStore.state.pastedFileName).replace(/{{number}}/g, '0'); + const file = new File([paste], `${fileName}.txt`, { type: 'text/plain' }); upload(file, `${fileName}.txt`); }); } @@ -731,7 +732,9 @@ async function post(ev?: MouseEvent) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index 95eb367318..8854babb6b 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -108,7 +108,7 @@ async function rename(file) { async function describe(file) { if (mock) return; - os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { default: file.comment !== null ? file.comment : '', file: file, }, { @@ -121,7 +121,8 @@ async function describe(file) { file.comment = comment; }); }, - }, 'closed'); + closed: () => dispose(), + }); } async function crop(file: Misskey.entities.DriveFile): Promise { diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue index 15f8128e98..1eae642937 100644 --- a/packages/frontend/src/components/MkRange.vue +++ b/packages/frontend/src/components/MkRange.vue @@ -101,17 +101,19 @@ const steps = computed(() => { } }); -const onMousedown = (ev: MouseEvent | TouchEvent) => { +function onMousedown(ev: MouseEvent | TouchEvent) { ev.preventDefault(); const tooltipShowing = ref(true); - os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), { showing: tooltipShowing, text: computed(() => { return props.textConverter(finalValue.value); }), targetElement: thumbEl, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); const style = document.createElement('style'); style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }')); @@ -152,7 +154,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => { window.addEventListener('touchmove', onDrag); window.addEventListener('mouseup', onMouseup, { once: true }); window.addEventListener('touchend', onMouseup, { once: true }); -}; +}