diff --git a/.github/workflows/api-misskey-js.yml b/.github/workflows/api-misskey-js.yml index 0dcf9ea9d7..19987d2af4 100644 --- a/.github/workflows/api-misskey-js.yml +++ b/.github/workflows/api-misskey-js.yml @@ -16,10 +16,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 - name: Setup Node.js uses: actions/setup-node@v6.4.0 diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml index f70a00ffd0..318999f596 100644 --- a/.github/workflows/changelog-check.yml +++ b/.github/workflows/changelog-check.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout head - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Setup Node.js uses: actions/setup-node@v6.4.0 with: diff --git a/.github/workflows/check-misskey-js-autogen.yml b/.github/workflows/check-misskey-js-autogen.yml index de3757b0e9..c5350b62d2 100644 --- a/.github/workflows/check-misskey-js-autogen.yml +++ b/.github/workflows/check-misskey-js-autogen.yml @@ -18,7 +18,7 @@ jobs: if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }} steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 with: submodules: true persist-credentials: false @@ -66,7 +66,7 @@ jobs: if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }} steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 with: submodules: true persist-credentials: false diff --git a/.github/workflows/check-misskey-js-version.yml b/.github/workflows/check-misskey-js-version.yml index f6095110c9..71351972f4 100644 --- a/.github/workflows/check-misskey-js-version.yml +++ b/.github/workflows/check-misskey-js-version.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Check version run: | if [ "$(jq -r '.version' package.json)" != "$(jq -r '.version' packages/misskey-js/package.json)" ]; then diff --git a/.github/workflows/check-spdx-license-id.yml b/.github/workflows/check-spdx-license-id.yml index d1448fc5d0..f75db131f0 100644 --- a/.github/workflows/check-spdx-license-id.yml +++ b/.github/workflows/check-spdx-license-id.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Check run: | counter=0 diff --git a/.github/workflows/check_copyright_year.yml b/.github/workflows/check_copyright_year.yml index 7514358929..4b5e71c43a 100644 --- a/.github/workflows/check_copyright_year.yml +++ b/.github/workflows/check_copyright_year.yml @@ -10,7 +10,7 @@ jobs: check_copyright_year: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 - run: | if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then echo "Please change copyright year!" diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml index f43c565ffd..983f664917 100644 --- a/.github/workflows/docker-develop.yml +++ b/.github/workflows/docker-develop.yml @@ -27,7 +27,7 @@ jobs: platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Check out the repo - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Log in to Docker Hub diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index c468b3ab17..277c88894e 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -32,7 +32,7 @@ jobs: platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Check out the repo - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Docker meta diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml index 916b905fd0..87256c000e 100644 --- a/.github/workflows/dockle.yml +++ b/.github/workflows/dockle.yml @@ -17,7 +17,7 @@ jobs: DOCKLE_VERSION: 0.4.15 steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 - name: Download and install dockle v${{ env.DOCKLE_VERSION }} run: | diff --git a/.github/workflows/frontend-bundle-report.yml b/.github/workflows/frontend-bundle-report.yml index 7625f4696d..b5af3bb0e1 100644 --- a/.github/workflows/frontend-bundle-report.yml +++ b/.github/workflows/frontend-bundle-report.yml @@ -41,7 +41,7 @@ jobs: FRONTEND_JS_SIZE_LOCALE: ja-JP steps: - name: Checkout base - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 with: repository: ${{ github.event.pull_request.base.repo.full_name }} ref: ${{ github.event.pull_request.base.sha }} @@ -49,7 +49,7 @@ jobs: submodules: true - name: Checkout pull request - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.sha }} @@ -69,7 +69,7 @@ jobs: - name: Setup pnpm if: steps.check-base-visualizer.outputs.supported == 'true' - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 with: package_json_file: after/package.json diff --git a/.github/workflows/get-api-diff.yml b/.github/workflows/get-api-diff.yml index a226bad9dc..ef34d19e68 100644 --- a/.github/workflows/get-api-diff.yml +++ b/.github/workflows/get-api-diff.yml @@ -25,12 +25,12 @@ jobs: ref: refs/pull/${{ github.event.number }}/merge steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 with: ref: ${{ matrix.ref }} submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 - name: Use Node.js uses: actions/setup-node@v6.4.0 with: diff --git a/.github/workflows/get-backend-memory.yml b/.github/workflows/get-backend-memory.yml index c845cc81fd..400a81a7d9 100644 --- a/.github/workflows/get-backend-memory.yml +++ b/.github/workflows/get-backend-memory.yml @@ -39,19 +39,19 @@ jobs: steps: - name: Checkout base - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 with: ref: ${{ github.base_ref }} path: base submodules: true - name: Checkout head - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 with: ref: refs/pull/${{ github.event.number }}/merge path: head submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 with: package_json_file: head/package.json - name: Use Node.js diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2a7b5226f9..7fe94eefa5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -36,12 +36,12 @@ jobs: pnpm_install: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 with: fetch-depth: 0 submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 - uses: actions/setup-node@v6.4.0 with: node-version-file: '.node-version' @@ -69,12 +69,12 @@ jobs: eslint-cache-version: v1 eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }} steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 with: fetch-depth: 0 submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 - uses: actions/setup-node@v6.4.0 with: node-version-file: '.node-version' @@ -100,12 +100,12 @@ jobs: - sw - misskey-js steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 with: fetch-depth: 0 submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 - uses: actions/setup-node@v6.4.0 with: node-version-file: '.node-version' diff --git a/.github/workflows/locale.yml b/.github/workflows/locale.yml index 5b2180f8aa..000cfed91d 100644 --- a/.github/workflows/locale.yml +++ b/.github/workflows/locale.yml @@ -16,12 +16,12 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 with: fetch-depth: 0 submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 - uses: actions/setup-node@v6.4.0 with: node-version-file: ".node-version" diff --git a/.github/workflows/on-release-created.yml b/.github/workflows/on-release-created.yml index 1e90fd87d5..76fd5548f2 100644 --- a/.github/workflows/on-release-created.yml +++ b/.github/workflows/on-release-created.yml @@ -16,11 +16,11 @@ jobs: id-token: write steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 with: submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 - name: Use Node.js uses: actions/setup-node@v6.4.0 with: diff --git a/.github/workflows/report-backend-memory.yml b/.github/workflows/report-backend-memory.yml index b3a98e046c..3343ec745e 100644 --- a/.github/workflows/report-backend-memory.yml +++ b/.github/workflows/report-backend-memory.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Download artifacts uses: actions/download-artifact@v8 diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml index af7fd535ad..70bfa329a1 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/storybook.yml @@ -22,12 +22,12 @@ jobs: NODE_OPTIONS: "--max_old_space_size=7168" steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 if: github.event_name != 'pull_request_target' with: fetch-depth: 0 submodules: true - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 if: github.event_name == 'pull_request_target' with: fetch-depth: 0 @@ -37,7 +37,7 @@ jobs: if: github.event_name == 'pull_request_target' run: git checkout "$(git rev-list --parents -n1 HEAD | cut -d" " -f3)" - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 - name: Use Node.js uses: actions/setup-node@v6.4.0 with: diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 198451c0e9..3bf3b9eff5 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -43,7 +43,7 @@ jobs: ports: - 56312:6379 meilisearch: - image: getmeili/meilisearch:v1.42.1 + image: getmeili/meilisearch:v1.48.1 ports: - 57712:7700 env: @@ -51,11 +51,11 @@ jobs: MEILI_ENV: development steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 with: submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 - name: Install FFmpeg run: | sudo apt install -y ffmpeg @@ -103,11 +103,11 @@ jobs: - 56312:6379 steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 with: submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 - name: Use Node.js uses: actions/setup-node@v6.4.0 with: @@ -147,11 +147,11 @@ jobs: POSTGRES_HOST_AUTH_METHOD: trust steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 with: submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 - name: Get current date id: current-date run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT diff --git a/.github/workflows/test-federation.yml b/.github/workflows/test-federation.yml index 0433931531..14dc33f626 100644 --- a/.github/workflows/test-federation.yml +++ b/.github/workflows/test-federation.yml @@ -30,7 +30,7 @@ jobs: with: submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 - name: Install FFmpeg run: | sudo apt install -y ffmpeg diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index 017de05736..6e55760462 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -28,11 +28,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 with: submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 - name: Use Node.js uses: actions/setup-node@v6.4.0 with: @@ -76,7 +76,7 @@ jobs: - 56312:6379 steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 with: submodules: true # https://github.com/cypress-io/cypress-docker-images/issues/150 @@ -86,7 +86,7 @@ jobs: #- uses: browser-actions/setup-firefox@latest # if: ${{ matrix.browser == 'firefox' }} - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 - name: Use Node.js uses: actions/setup-node@v6.4.0 with: @@ -105,7 +105,7 @@ jobs: - name: Cypress install run: pnpm exec cypress install - name: Cypress run - uses: cypress-io/github-action@v7.1.9 + uses: cypress-io/github-action@v7.4.0 timeout-minutes: 15 with: install: false diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml index e3187ba7fe..f3c6f0a516 100644 --- a/.github/workflows/test-misskey-js.yml +++ b/.github/workflows/test-misskey-js.yml @@ -22,10 +22,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 - name: Setup Node.js uses: actions/setup-node@v6.4.0 diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml index 18960f45c7..0d9b2293dc 100644 --- a/.github/workflows/test-production.yml +++ b/.github/workflows/test-production.yml @@ -16,11 +16,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 with: submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 - name: Use Node.js uses: actions/setup-node@v6.4.0 with: diff --git a/.github/workflows/validate-api-json.yml b/.github/workflows/validate-api-json.yml index e348af53eb..ada1b3b2d8 100644 --- a/.github/workflows/validate-api-json.yml +++ b/.github/workflows/validate-api-json.yml @@ -17,11 +17,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 with: submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v6.0.3 + uses: pnpm/action-setup@v6.0.9 - name: Use Node.js uses: actions/setup-node@v6.4.0 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index dbee019649..bad2db5ce2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,14 +17,17 @@ - Feat: コントロールパネルから二要素認証を解除できるように ### Client +- Fix: チャットでIMEの変換を確定するEnterでメッセージが送信されてしまうことがある問題を修正 - 2025.4.0 以前の設定情報の移行処理が削除されました - 2025.4.0 から直接 2026.6.0 以上にアップデートする場合は設定が移行されませんので注意してください。移行したい場合は一度 2026.5.1 を経由してください。 +- Fix: デバイスタイプをスマートフォンに固定している状態で画面幅が広いとき、画面左上のアイコンが表示されない問題を修正 ### Server - Enhance: センシティブメディアの判定を外部サービス ([sensitive-detector](https://github.com/misskey-dev/sensitive-detector)) に分離し、`nsfwjs` / `@tensorflow/tfjs(-node)` の同梱と NSFW 判定モデルを廃止 (#16804) - Enhance: Node.js 22.23.0以降、24.17.0以降、26.4.0以降をサポートするように - Enhance: Docker Image の Node.js を 26.4.0 に、Debian を trixie (v13) に更新 - Fix: `/stats` API のレスポンス型が正しくない問題を修正 +- Fix: ハッシュタグに関連するデータを更新する際のエラーハンドリングを修正 ## 2026.6.0 diff --git a/package.json b/package.json index 2682e851f7..33925d773a 100644 --- a/package.json +++ b/package.json @@ -53,14 +53,11 @@ "cleanall": "pnpm clean-all" }, "dependencies": { - "cssnano": "8.0.2", "esbuild": "0.28.1", "execa": "9.6.1", "ignore-walk": "9.0.0", "js-yaml": "4.2.0", - "postcss": "8.5.15", - "tar": "7.5.16", - "terser": "5.48.0" + "tar": "7.5.16" }, "devDependencies": { "@eslint/js": "9.39.4", diff --git a/packages/backend/package.json b/packages/backend/package.json index 831f2e156a..58b4df7158 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -128,7 +128,6 @@ "reflect-metadata": "0.2.2", "rename": "1.0.4", "rss-parser": "3.13.0", - "rxjs": "7.8.2", "sanitize-html": "2.17.5", "secure-json-parse": "4.1.0", "semver": "7.8.5", diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts index 793bbeecb1..beed786d1b 100644 --- a/packages/backend/src/core/HashtagService.ts +++ b/packages/backend/src/core/HashtagService.ts @@ -5,20 +5,28 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; +import { DataSource } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { MiUser } from '@/models/User.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { IdService } from '@/core/IdService.js'; -import type { MiHashtag } from '@/models/Hashtag.js'; +import { MiHashtag } from '@/models/Hashtag.js'; import type { HashtagsRepository, MiMeta } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { FeaturedService } from '@/core/FeaturedService.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; +import Logger from '../logger.js'; + +const logger = new Logger('hashtag/create'); @Injectable() export class HashtagService { constructor( + @Inject(DI.db) + private db: DataSource, + @Inject(DI.meta) private meta: MiMeta, @@ -60,19 +68,74 @@ export class HashtagService { // TODO: サンプリング this.updateHashtagsRanking(tag, user.id); - const index = await this.hashtagsRepository.findOneBy({ name: tag }); + { + const index = await this.hashtagsRepository.findOneBy({ name: tag }); - if (index == null && !inc) return; + if (index == null && inc) { + try { + if (isUserAttached) { + await this.hashtagsRepository.insert({ + id: this.idService.gen(), + name: tag, + mentionedUserIds: [], + mentionedUsersCount: 0, + mentionedLocalUserIds: [], + mentionedLocalUsersCount: 0, + mentionedRemoteUserIds: [], + mentionedRemoteUsersCount: 0, + attachedUserIds: [user.id], + attachedUsersCount: 1, + attachedLocalUserIds: this.userEntityService.isLocalUser(user) ? [user.id] : [], + attachedLocalUsersCount: this.userEntityService.isLocalUser(user) ? 1 : 0, + attachedRemoteUserIds: this.userEntityService.isRemoteUser(user) ? [user.id] : [], + attachedRemoteUsersCount: this.userEntityService.isRemoteUser(user) ? 1 : 0, + } as MiHashtag); + } else { + await this.hashtagsRepository.insert({ + id: this.idService.gen(), + name: tag, + mentionedUserIds: [user.id], + mentionedUsersCount: 1, + mentionedLocalUserIds: this.userEntityService.isLocalUser(user) ? [user.id] : [], + mentionedLocalUsersCount: this.userEntityService.isLocalUser(user) ? 1 : 0, + mentionedRemoteUserIds: this.userEntityService.isRemoteUser(user) ? [user.id] : [], + mentionedRemoteUsersCount: this.userEntityService.isRemoteUser(user) ? 1 : 0, + attachedUserIds: [], + attachedUsersCount: 0, + attachedLocalUserIds: [], + attachedLocalUsersCount: 0, + attachedRemoteUserIds: [], + attachedRemoteUsersCount: 0, + } as MiHashtag); + } + return; + } catch (err) { + if (isDuplicateKeyValueError(err)) { + logger.info(`Duplicate insertion detected. Falling back to update. #${tag}`); + } else { + throw err; + } + } + } + } - if (index != null) { - const q = this.hashtagsRepository.createQueryBuilder('tag').update() - .where('name = :name', { name: tag }); + await this.db.transaction(async transactionalEntityManager => { + const transactionalHashtagRepository = transactionalEntityManager + .getRepository(MiHashtag); + + const index = await transactionalHashtagRepository + .createQueryBuilder() + .setLock('pessimistic_write') + .where('name = :name', { name: tag }) + .getOne(); + + if (index == null) return; const set = {} as any; if (isUserAttached) { if (inc) { - // 自分が初めてこのタグを使ったなら + // 自分が初めてこのタグを使ったなら if (!index.attachedUserIds.some(id => id === user.id)) { set.attachedUserIds = () => `array_append("attachedUserIds", '${user.id}')`; set.attachedUsersCount = () => '"attachedUsersCount" + 1'; @@ -117,46 +180,14 @@ export class HashtagService { } if (Object.keys(set).length > 0) { - q.set(set); - q.execute(); + await transactionalHashtagRepository + .createQueryBuilder() + .update() + .where('id = :id', { id: index.id }) + .set(set) + .execute(); } - } else { - if (isUserAttached) { - this.hashtagsRepository.insert({ - id: this.idService.gen(), - name: tag, - mentionedUserIds: [], - mentionedUsersCount: 0, - mentionedLocalUserIds: [], - mentionedLocalUsersCount: 0, - mentionedRemoteUserIds: [], - mentionedRemoteUsersCount: 0, - attachedUserIds: [user.id], - attachedUsersCount: 1, - attachedLocalUserIds: this.userEntityService.isLocalUser(user) ? [user.id] : [], - attachedLocalUsersCount: this.userEntityService.isLocalUser(user) ? 1 : 0, - attachedRemoteUserIds: this.userEntityService.isRemoteUser(user) ? [user.id] : [], - attachedRemoteUsersCount: this.userEntityService.isRemoteUser(user) ? 1 : 0, - } as MiHashtag); - } else { - this.hashtagsRepository.insert({ - id: this.idService.gen(), - name: tag, - mentionedUserIds: [user.id], - mentionedUsersCount: 1, - mentionedLocalUserIds: this.userEntityService.isLocalUser(user) ? [user.id] : [], - mentionedLocalUsersCount: this.userEntityService.isLocalUser(user) ? 1 : 0, - mentionedRemoteUserIds: this.userEntityService.isRemoteUser(user) ? [user.id] : [], - mentionedRemoteUsersCount: this.userEntityService.isRemoteUser(user) ? 1 : 0, - attachedUserIds: [], - attachedUsersCount: 0, - attachedLocalUserIds: [], - attachedLocalUsersCount: 0, - attachedRemoteUserIds: [], - attachedRemoteUsersCount: 0, - } as MiHashtag); - } - } + }); } @bindThis diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts index 791b4f1d9b..2e55afc264 100644 --- a/packages/backend/test/e2e/move.ts +++ b/packages/backend/test/e2e/move.ts @@ -7,9 +7,8 @@ import { INestApplicationContext } from '@nestjs/common'; process.env.NODE_ENV = 'test'; -import { setTimeout } from 'node:timers/promises'; import * as assert from 'assert'; -import { afterAll, beforeAll, afterEach, describe, test } from 'vitest'; +import { afterAll, beforeAll, afterEach, describe, test, vi } from 'vitest'; import { loadConfig } from '@/config.js'; import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; @@ -17,6 +16,11 @@ import { jobQueue } from '@/boot/common.js'; import { api, castAsError, initTestDb, signup, successfulApiCall, uploadFile } from '../utils.js'; import type * as misskey from 'misskey-js'; +// i/move 呼び出しで大部分が同期的に完了する後続ジョブ (フォロー解除/ブロック・ミュート引き継ぎ/リスト更新等) を待つ +const waitForMoveJobOptions = { timeout: 5000, interval: 50 }; +// AccountMoveService がテスト環境向けに 10000ms に短縮している遅延アンフォロージョブを待つ (安全マージンを載せた上限) +const waitForDelayedUnfollowJobOptions = { timeout: 15000, interval: 100 }; + describe('Account Move', () => { let jq: INestApplicationContext; let url: URL; @@ -273,53 +277,63 @@ describe('Account Move', () => { assert.strictEqual(move.status, 200); - await setTimeout(1000 * 3); // wait for jobs to finish - // Unfollow delayed? - const aliceFollowings = await api('users/following', { - userId: alice.id, - }, alice); - assert.strictEqual(aliceFollowings.status, 200); - assert.ok(aliceFollowings); - assert.strictEqual(aliceFollowings.body.length, 3); + await vi.waitFor(async () => { + const aliceFollowings = await api('users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(aliceFollowings.status, 200); + assert.ok(aliceFollowings); + assert.strictEqual(aliceFollowings.body.length, 3); + }, waitForMoveJobOptions); - const carolFollowings = await api('users/following', { - userId: carol.id, - }, carol); - assert.strictEqual(carolFollowings.status, 200); - assert.ok(carolFollowings); - assert.strictEqual(carolFollowings.body.length, 2); - assert.strictEqual(carolFollowings.body[0].followeeId, bob.id); - assert.strictEqual(carolFollowings.body[1].followeeId, alice.id); + await vi.waitFor(async () => { + const carolFollowings = await api('users/following', { + userId: carol.id, + }, carol); + assert.strictEqual(carolFollowings.status, 200); + assert.ok(carolFollowings); + assert.strictEqual(carolFollowings.body.length, 2); + assert.strictEqual(carolFollowings.body[0].followeeId, bob.id); + assert.strictEqual(carolFollowings.body[1].followeeId, alice.id); + }, waitForMoveJobOptions); - const blockings = await api('blocking/list', {}, dave); - assert.strictEqual(blockings.status, 200); - assert.ok(blockings); - assert.strictEqual(blockings.body.length, 2); - assert.strictEqual(blockings.body[0].blockeeId, bob.id); - assert.strictEqual(blockings.body[1].blockeeId, alice.id); + await vi.waitFor(async () => { + const blockings = await api('blocking/list', {}, dave); + assert.strictEqual(blockings.status, 200); + assert.ok(blockings); + assert.strictEqual(blockings.body.length, 2); + assert.strictEqual(blockings.body[0].blockeeId, bob.id); + assert.strictEqual(blockings.body[1].blockeeId, alice.id); + }, waitForMoveJobOptions); - const mutings = await api('mute/list', {}, dave); - assert.strictEqual(mutings.status, 200); - assert.ok(mutings); - assert.strictEqual(mutings.body.length, 2); - assert.strictEqual(mutings.body[0].muteeId, bob.id); - assert.strictEqual(mutings.body[1].muteeId, alice.id); + await vi.waitFor(async () => { + const mutings = await api('mute/list', {}, dave); + assert.strictEqual(mutings.status, 200); + assert.ok(mutings); + assert.strictEqual(mutings.body.length, 2); + assert.strictEqual(mutings.body[0].muteeId, bob.id); + assert.strictEqual(mutings.body[1].muteeId, alice.id); + }, waitForMoveJobOptions); - const rootLists = await api('users/lists/list', {}, root); - assert.strictEqual(rootLists.status, 200); - assert.ok(rootLists); - assert.ok(rootLists.body[0].userIds); - assert.strictEqual(rootLists.body[0].userIds.length, 2); - assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id)); - assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id)); + await vi.waitFor(async () => { + const rootLists = await api('users/lists/list', {}, root); + assert.strictEqual(rootLists.status, 200); + assert.ok(rootLists); + assert.ok(rootLists.body[0].userIds); + assert.strictEqual(rootLists.body[0].userIds.length, 2); + assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id)); + assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id)); + }, waitForMoveJobOptions); - const eveLists = await api('users/lists/list', {}, eve); - assert.strictEqual(eveLists.status, 200); - assert.ok(eveLists); - assert.ok(eveLists.body[0].userIds); - assert.strictEqual(eveLists.body[0].userIds.length, 1); - assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id)); + await vi.waitFor(async () => { + const eveLists = await api('users/lists/list', {}, eve); + assert.strictEqual(eveLists.status, 200); + assert.ok(eveLists); + assert.ok(eveLists.body[0].userIds); + assert.strictEqual(eveLists.body[0].userIds.length, 1); + assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id)); + }, waitForMoveJobOptions); }); test('A locked account automatically accept the follow request if it had already accepted the old account.', async () => { @@ -340,14 +354,14 @@ describe('Account Move', () => { }); test('Unfollowed after 10 sec (24 hours in production).', async () => { - await setTimeout(1000 * 8); + await vi.waitFor(async () => { + const following = await api('users/following', { + userId: alice.id, + }, alice); - const following = await api('users/following', { - userId: alice.id, - }, alice); - - assert.strictEqual(following.status, 200); - assert.strictEqual(following.body.length, 0); + assert.strictEqual(following.status, 200); + assert.strictEqual(following.body.length, 0); + }, waitForDelayedUnfollowJobOptions); }); test('Unable to move if the destination account has already moved.', async () => { diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts index 18762ac383..a051be6c3c 100644 --- a/packages/backend/test/e2e/streaming.ts +++ b/packages/backend/test/e2e/streaming.ts @@ -8,9 +8,9 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { describe, beforeAll, test } from 'vitest'; import { WebSocket } from 'ws'; -import { MiFollowing } from '@/models/Following.js'; -import { api, createAppToken, initTestDb, port, post, signup, waitFire } from '../utils.js'; +import { api, connectStream, createAppToken, initTestDb, port, post, signup, waitFire } from '../utils.js'; import type * as misskey from 'misskey-js'; +import { MiFollowing } from '@/models/Following.js'; describe('Streaming', () => { let Followings: any; @@ -744,164 +744,83 @@ describe('Streaming', () => { assert.strictEqual(fired, true); }); - // XXX: QueryFailedError: duplicate key value violates unique constraint "IDX_347fec870eafea7b26c8a73bac" - /* describe('Hashtag Timeline', () => { - test('指定したハッシュタグの投稿が流れる', () => new Promise(async done => { - const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { - if (type === 'note') { - assert.deepStrictEqual(body.text, '#foo'); - ws.close(); - done(); - } - }, { - q: [ - ['foo'], - ], - }); + test('指定したハッシュタグの投稿が流れる', async () => { + const fired = await waitFire( + chitose, 'hashtag', + () => api('notes/create', { text: '#foo' }, chitose), + msg => msg.type === 'note' && msg.body.text === '#foo', + { q: [['foo']] }, + ); - post(chitose, { - text: '#foo', - }); - })); + assert.strictEqual(fired, true); + }); - test('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => { - let fooCount = 0; - let barCount = 0; - let fooBarCount = 0; + test('指定したハッシュタグの投稿が流れる (AND)', async () => { + const received: string[] = []; + const ws = await connectStream(chitose, 'hashtag', (msg) => { + if (msg.type === 'note') received.push(msg.body.text); + }, { q: [['foo', 'bar']] }); - const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { - if (type === 'note') { - if (body.text === '#foo') fooCount++; - if (body.text === '#bar') barCount++; - if (body.text === '#foo #bar') fooBarCount++; - } - }, { - q: [ - ['foo', 'bar'], - ], - }); + await Promise.all([ + await api('notes/create', { text: '#foo' }, chitose), + await api('notes/create', { text: '#bar' }, chitose), + await api('notes/create', { text: '#foo #bar' }, chitose), + ]); - post(chitose, { - text: '#foo', - }); + await new Promise(r => setTimeout(r, 1000)); + ws.close(); - post(chitose, { - text: '#bar', - }); + assert.strictEqual(received.includes('#foo'), false); + assert.strictEqual(received.includes('#bar'), false); + assert.strictEqual(received.includes('#foo #bar'), true); + }); - post(chitose, { - text: '#foo #bar', - }); + test('指定したハッシュタグの投稿が流れる (OR)', async () => { + const received: string[] = []; + const ws = await connectStream(chitose, 'hashtag', (msg) => { + if (msg.type === 'note') received.push(msg.body.text); + }, { q: [['foo'], ['bar']] }); - setTimeout(() => { - assert.strictEqual(fooCount, 0); - assert.strictEqual(barCount, 0); - assert.strictEqual(fooBarCount, 1); - ws.close(); - done(); - }, 3000); - })); + await Promise.all([ + await api('notes/create', { text: '#foo' }, chitose), + await api('notes/create', { text: '#bar' }, chitose), + await api('notes/create', { text: '#foo #bar' }, chitose), + await api('notes/create', { text: '#piyo' }, chitose), + ]); - test('指定したハッシュタグの投稿が流れる (OR)', () => new Promise(async done => { - let fooCount = 0; - let barCount = 0; - let fooBarCount = 0; - let piyoCount = 0; + await new Promise(r => setTimeout(r, 1000)); + ws.close(); - const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { - if (type === 'note') { - if (body.text === '#foo') fooCount++; - if (body.text === '#bar') barCount++; - if (body.text === '#foo #bar') fooBarCount++; - if (body.text === '#piyo') piyoCount++; - } - }, { - q: [ - ['foo'], - ['bar'], - ], - }); + assert.strictEqual(received.includes('#foo'), true); + assert.strictEqual(received.includes('#bar'), true); + assert.strictEqual(received.includes('#foo #bar'), true); + assert.strictEqual(received.includes('#piyo'), false); + }); - post(chitose, { - text: '#foo', - }); + test('指定したハッシュタグの投稿が流れる (AND + OR)', async () => { + const received: string[] = []; + const ws = await connectStream(chitose, 'hashtag', (msg) => { + if (msg.type === 'note') received.push(msg.body.text); + }, { q: [['foo', 'bar'], ['piyo']] }); - post(chitose, { - text: '#bar', - }); + await Promise.all([ + api('notes/create', { text: '#foo' }, chitose), + api('notes/create', { text: '#bar' }, chitose), + api('notes/create', { text: '#foo #bar' }, chitose), + api('notes/create', { text: '#piyo' }, chitose), + api('notes/create', { text: '#waaa' }, chitose), + ]); - post(chitose, { - text: '#foo #bar', - }); + await new Promise(r => setTimeout(r, 1000)); + ws.close(); - post(chitose, { - text: '#piyo', - }); - - setTimeout(() => { - assert.strictEqual(fooCount, 1); - assert.strictEqual(barCount, 1); - assert.strictEqual(fooBarCount, 1); - assert.strictEqual(piyoCount, 0); - ws.close(); - done(); - }, 3000); - })); - - test('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise(async done => { - let fooCount = 0; - let barCount = 0; - let fooBarCount = 0; - let piyoCount = 0; - let waaaCount = 0; - - const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { - if (type === 'note') { - if (body.text === '#foo') fooCount++; - if (body.text === '#bar') barCount++; - if (body.text === '#foo #bar') fooBarCount++; - if (body.text === '#piyo') piyoCount++; - if (body.text === '#waaa') waaaCount++; - } - }, { - q: [ - ['foo', 'bar'], - ['piyo'], - ], - }); - - post(chitose, { - text: '#foo', - }); - - post(chitose, { - text: '#bar', - }); - - post(chitose, { - text: '#foo #bar', - }); - - post(chitose, { - text: '#piyo', - }); - - post(chitose, { - text: '#waaa', - }); - - setTimeout(() => { - assert.strictEqual(fooCount, 0); - assert.strictEqual(barCount, 0); - assert.strictEqual(fooBarCount, 1); - assert.strictEqual(piyoCount, 1); - assert.strictEqual(waaaCount, 0); - ws.close(); - done(); - }, 3000); - })); + assert.strictEqual(received.includes('#foo'), false); + assert.strictEqual(received.includes('#bar'), false); + assert.strictEqual(received.includes('#foo #bar'), true); + assert.strictEqual(received.includes('#piyo'), true); + assert.strictEqual(received.includes('#waaa'), false); + }); }); - */ }); }); diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index 8a23657772..b777270f8b 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -9,7 +9,7 @@ // pnpm jest -- e2e/timelines.ts import * as assert from 'assert'; -import { describe, beforeAll, test } from 'vitest'; +import { describe, beforeAll, test, vi } from 'vitest'; import { setTimeout } from 'node:timers/promises'; import { entities } from 'misskey-js'; import { Redis } from 'ioredis'; @@ -21,6 +21,8 @@ function genHost() { return randomString() + '.example.com'; } +const waitForPushToTlOptions = { timeout: 3000, interval: 25 }; + let redisForTimelines: Redis; let root: SignupResponse; @@ -108,52 +110,49 @@ describe('Timelines', () => { const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); + }, waitForPushToTlOptions); }); test('フォローしているユーザーのノートが含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); const bobNote = await post(bob, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }, waitForPushToTlOptions); }); test('フォローしているユーザーの visibility: followers なノートが含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); const carolNote = await post(carol, { text: 'hi' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }, waitForPushToTlOptions); }); test('withReplies: false でフォローしているユーザーの他人への返信が含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -172,16 +171,15 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }, waitForPushToTlOptions); }); test('withReplies: true でフォローしているユーザーの他人へのDM返信が含まれない', async () => { @@ -189,7 +187,6 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); @@ -207,7 +204,6 @@ describe('Timelines', () => { await api('following/create', { userId: carol.id }, bob); await api('following/create', { userId: bob.id }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -228,17 +224,16 @@ 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 setTimeout(250); const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); - assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'hi'); + }, waitForPushToTlOptions); }); test('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => { @@ -249,16 +244,15 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: alice.id }, bob); await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(250); const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + }, waitForPushToTlOptions); }); test('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => { @@ -267,32 +261,30 @@ 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 setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + }, waitForPushToTlOptions); }); test('withReplies: false でフォローしているユーザーのそのユーザー自身への返信が含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + }, waitForPushToTlOptions); }); test('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => { @@ -301,16 +293,15 @@ describe('Timelines', () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('自分の他人への返信が含まれる', async () => { @@ -321,54 +312,51 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'hi' }); const aliceNote = await post(alice, { text: 'hi', replyId: bobNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + }, waitForPushToTlOptions); }); test('フォローしているユーザーの他人の投稿のリノートが含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { renoteId: carolNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }, waitForPushToTlOptions); }); test('[withRenotes: false] フォローしているユーザーの投稿が含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); const bobNote = await post(bob, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { + limit: 100, + withRenotes: false, + }, alice); - const res = await api('notes/timeline', { - limit: 100, - withRenotes: false, - }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }, waitForPushToTlOptions); }); test('[withRenotes: false] フォローしているユーザーのファイルのみの投稿が含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); 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'), @@ -376,22 +364,21 @@ describe('Timelines', () => { const bobNote = await post(bob, { fileIds: [bobFile.id] }); const carolNote = await post(carol, { fileIds: [carolFile.id] }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { + limit: 100, + withRenotes: false, + }, alice); - const res = await api('notes/timeline', { - limit: 100, - withRenotes: false, - }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }, waitForPushToTlOptions); }); test('[withRenotes: false] フォローしているユーザーの他人の投稿のリノートが含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { renoteId: carolNote.id }); @@ -409,25 +396,23 @@ describe('Timelines', () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { + withRenotes: false, + }, alice); - const res = await api('notes/timeline', { - withRenotes: false, - }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }, waitForPushToTlOptions); }); test('フォローしているユーザーの他人への visibility: specified なノートが含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); await waitForPushToTl(); @@ -442,7 +427,6 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('mute/create', { userId: carol.id }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); @@ -460,7 +444,6 @@ 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 setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -477,7 +460,6 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('mute/create', { userId: carol.id }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id }); const bobNote = await post(bob, { renoteId: daveNote.id }); @@ -496,7 +478,6 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('mute/create', { userId: carol.id }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id }); const bobNote = await post(bob, { renoteId: daveNote.id }); @@ -518,11 +499,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'hi' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => { @@ -533,18 +514,17 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('[withFiles: true] フォローしているユーザーのファイル付きノートのみ含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); 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'), @@ -554,14 +534,14 @@ describe('Timelines', () => { const carolNote1 = await post(carol, { text: 'hi' }); const carolNote2 = await post(carol, { fileIds: [carolFile.id] }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100, withFiles: true }, alice); - const res = await api('notes/timeline', { limit: 100, withFiles: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false); + }, waitForPushToTlOptions); }, 1000 * 30); test('フォローしているユーザーのチャンネル投稿が含まれない', async () => { @@ -569,7 +549,6 @@ describe('Timelines', () => { const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); await waitForPushToTl(); @@ -584,27 +563,26 @@ describe('Timelines', () => { const aliceNote = await post(alice, { text: 'hi', visibility: 'specified' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); + }, waitForPushToTlOptions); }); test('フォローしているユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + }, waitForPushToTlOptions); }); test('フォローしていないユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれない', async () => { @@ -623,7 +601,6 @@ describe('Timelines', () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); await waitForPushToTl(); @@ -641,12 +618,12 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); const aliceNote = await post(alice, { text: 'ok', visibility: 'specified', visibleUserIds: [bob.id], replyId: bobNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'ok'); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'ok'); + }, waitForPushToTlOptions); }); /* TODO @@ -654,10 +631,11 @@ describe('Timelines', () => { const [alice, bob] = await Promise.all([signup(), signup()]); const aliceNote = await post(alice, { text: 'hi', visibility: 'specified', visibleUserIds: [bob.id] }); const bobNote = await post(bob, { text: 'ok', visibility: 'specified', visibleUserIds: [alice.id], replyId: aliceNote.id }); - await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id).text, 'ok'); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id).text, 'ok'); + }, waitForPushToTlOptions); }); */ @@ -700,11 +678,11 @@ describe('Timelines', () => { const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('チャンネル未フォロー + ユーザフォロー = TLに流れない', async () => { @@ -733,11 +711,11 @@ describe('Timelines', () => { const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('チャンネル未フォロー + ユーザ未フォロー + チャンネルミュート = TLに流れない', async () => { @@ -851,11 +829,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); const bobRenote = await renote(bobNote.id, bob); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + }, waitForPushToTlOptions); }); test('[チャンネル外リノート] チャンネルフォロー + ユーザフォロー = TLに流れる', async () => { @@ -869,11 +847,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); const bobRenote = await renote(bobNote.id, bob); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + }, waitForPushToTlOptions); }); test('[チャンネル外リノート] チャンネル未フォロー + ユーザ未フォロー + チャンネルミュート = TLに流れない', async () => { @@ -960,19 +938,19 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'I\'m Bob.' }); const carolNote = await post(carol, { text: 'I\'m Carol.' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + if (enableFanoutTimeline) { + // NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる + assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 1); - if (enableFanoutTimeline) { - // NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる - assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 1); - - const bobHTL = await redisForTimelines.lrange(`list:homeTimeline:${bob.id}`, 0, -1); - assert.strictEqual(bobHTL.includes(aliceNote.id), true); - assert.strictEqual(bobHTL.includes(bobNote.id), true); - assert.strictEqual(bobHTL.includes(carolNote.id), false); - } else { - assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 0); - } + const bobHTL = await redisForTimelines.lrange(`list:homeTimeline:${bob.id}`, 0, -1); + assert.strictEqual(bobHTL.includes(aliceNote.id), true); + assert.strictEqual(bobHTL.includes(bobNote.id), true); + assert.strictEqual(bobHTL.includes(carolNote.id), false); + } else { + assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 0); + } + }, waitForPushToTlOptions); }); test('FTT: リモートユーザーの HTL にはプッシュされない', async () => { @@ -985,10 +963,10 @@ describe('Timelines', () => { await post(alice, { text: 'I\'m Alice.' }); await post(bob, { text: 'I\'m Bob.' }); - await waitForPushToTl(); - - // NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる - assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 0); + await vi.waitFor(async () => { + // NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる + assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 0); + }, waitForPushToTlOptions); }); describe('凍結', () => { @@ -1004,7 +982,13 @@ describe('Timelines', () => { bobNote = await post(bob, { text: 'yo' }); carolNote = await post(carol, { text: 'kon\'nichiwa' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); + + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + }, waitForPushToTlOptions); await api('admin/suspend-user', { userId: carol.id }, root); await setTimeout(100); @@ -1045,7 +1029,15 @@ describe('Timelines', () => { bobRenote = await post(bob, { renoteId: carolNote.id }); carolRenote = await post(carol, { renoteId: bobNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); + + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolRenote.id), true); + }, waitForPushToTlOptions); await api('admin/suspend-user', { userId: carol.id }, root); await setTimeout(100); @@ -1088,7 +1080,13 @@ describe('Timelines', () => { bobNote = await post(bob, { text: 'yo' }); carolNote = await post(carol, { text: 'kon\'nichiwa' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); + + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + }, waitForPushToTlOptions); await api('admin/suspend-user', { userId: carol.id }, root); await setTimeout(100); @@ -1122,12 +1120,12 @@ describe('Timelines', () => { const carolNote = await post(carol, { text: 'hi', visibility: 'home' }); const bobNote = await post(bob, { text: 'hi' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/local-timeline', { limit: 100 }, alice); - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }, waitForPushToTlOptions); }); test('他人の他人への返信が含まれない', async () => { @@ -1136,12 +1134,12 @@ describe('Timelines', () => { const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/local-timeline', { limit: 100 }, alice); - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + }, waitForPushToTlOptions); }); test('他人のその人自身への返信が含まれる', async () => { @@ -1150,12 +1148,12 @@ describe('Timelines', () => { const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/local-timeline', { limit: 100 }, alice); - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + }, waitForPushToTlOptions); }); test('チャンネル投稿が含まれない', async () => { @@ -1188,32 +1186,30 @@ describe('Timelines', () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: carol.id }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi', visibility: 'home' }); const bobNote = await post(bob, { text: 'hi' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/local-timeline', { limit: 100 }, alice); - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }, waitForPushToTlOptions); }); test('ミュートしているユーザーのノートが含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('mute/create', { userId: carol.id }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/local-timeline', { limit: 100 }, alice); - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }, waitForPushToTlOptions); }); test('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => { @@ -1221,7 +1217,6 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('mute/create', { userId: carol.id }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); @@ -1239,7 +1234,6 @@ 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 setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -1255,7 +1249,6 @@ describe('Timelines', () => { const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); await api('mute/create', { userId: carol.id }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id }); const bobNote = await post(bob, { renoteId: daveNote.id }); @@ -1273,7 +1266,6 @@ describe('Timelines', () => { const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); await api('mute/create', { userId: carol.id }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id }); const bobNote = await post(bob, { renoteId: daveNote.id }); @@ -1293,16 +1285,15 @@ describe('Timelines', () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/local-timeline', { limit: 100 }, alice); - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => { @@ -1310,16 +1301,15 @@ describe('Timelines', () => { const [alice, bob] = await Promise.all([signup(), signup()]); - await setTimeout(250); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/local-timeline', { limit: 100 }, alice); - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('[withReplies: true] 他人の他人への返信が含まれる', async () => { @@ -1328,11 +1318,11 @@ describe('Timelines', () => { const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/local-timeline', { limit: 100, withReplies: true }, alice); - const res = await api('notes/local-timeline', { limit: 100, withReplies: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('[withFiles: true] ファイル付きノートのみ含まれる', async () => { @@ -1342,12 +1332,12 @@ describe('Timelines', () => { const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { fileIds: [file.id] }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/local-timeline', { limit: 100, withFiles: true }, alice); - const res = await api('notes/local-timeline', { limit: 100, withFiles: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + }, waitForPushToTlOptions); }, 1000 * 10); describe('Channel', () => { @@ -1492,11 +1482,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); const bobRenote = await renote(bobNote.id, bob); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/local-timeline', { limit: 100 }, alice); - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + }, waitForPushToTlOptions); }); test('[チャンネル外リノート] チャンネルフォロー + ユーザ未フォロー = TLに流れる', async () => { @@ -1509,11 +1499,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); const bobRenote = await renote(bobNote.id, bob); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/local-timeline', { limit: 100 }, alice); - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + }, waitForPushToTlOptions); }); test('[チャンネル外リノート] チャンネル未フォロー + ユーザフォロー = TLに流れる', async () => { @@ -1526,11 +1516,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); const bobRenote = await renote(bobNote.id, bob); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/local-timeline', { limit: 100 }, alice); - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + }, waitForPushToTlOptions); }); test('[チャンネル外リノート] チャンネルフォロー + ユーザフォロー = TLに流れる', async () => { @@ -1544,11 +1534,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); const bobRenote = await renote(bobNote.id, bob); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/local-timeline', { limit: 100 }, alice); - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + }, waitForPushToTlOptions); }); test('[チャンネル外リノート] チャンネル未フォロー + ユーザ未フォロー + チャンネルミュート = TLに流れない', async () => { @@ -1631,11 +1621,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'hi' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('ローカルユーザーの visibility: home なノートが含まれない', async () => { @@ -1654,14 +1644,13 @@ describe('Timelines', () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => { @@ -1671,16 +1660,15 @@ describe('Timelines', () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => { @@ -1689,7 +1677,6 @@ describe('Timelines', () => { await api('following/create', { userId: carol.id }, bob); await api('following/create', { userId: bob.id }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -1711,17 +1698,16 @@ 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 setTimeout(250); const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); - assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id)?.text, 'hi'); + }, waitForPushToTlOptions); }); test('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => { @@ -1733,16 +1719,15 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: alice.id }, bob); await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(250); const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + }, waitForPushToTlOptions); }); test('他人の他人への返信が含まれない', async () => { @@ -1751,12 +1736,12 @@ describe('Timelines', () => { const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + }, waitForPushToTlOptions); }); test('リモートユーザーのノートが含まれない', async () => { @@ -1779,11 +1764,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'hi' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => { @@ -1794,11 +1779,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => { @@ -1807,16 +1792,15 @@ describe('Timelines', () => { const [alice, bob] = await Promise.all([signup(), signup()]); - await setTimeout(250); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/local-timeline', { limit: 100 }, alice); - const res = await api('notes/local-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('[withReplies: true] 他人の他人への返信が含まれる', async () => { @@ -1825,11 +1809,11 @@ describe('Timelines', () => { const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100, withReplies: true }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100, withReplies: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('[withFiles: true] ファイル付きノートのみ含まれる', async () => { @@ -1839,12 +1823,12 @@ describe('Timelines', () => { const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { fileIds: [file.id] }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100, withFiles: true }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100, withFiles: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + }, waitForPushToTlOptions); }, 1000 * 10); describe('Channel', () => { @@ -1872,11 +1856,11 @@ describe('Timelines', () => { const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('チャンネル未フォロー + ユーザフォロー = TLに流れない', async () => { @@ -1905,11 +1889,11 @@ describe('Timelines', () => { const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('チャンネル未フォロー + ユーザ未フォロー + チャンネルミュート = TLに流れない', async () => { @@ -1989,11 +1973,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); const bobRenote = await renote(bobNote.id, bob); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + }, waitForPushToTlOptions); }); test('[チャンネル外リノート] チャンネルフォロー + ユーザ未フォロー = TLに流れる', async () => { @@ -2006,11 +1990,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); const bobRenote = await renote(bobNote.id, bob); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + }, waitForPushToTlOptions); }); test('[チャンネル外リノート] チャンネル未フォロー + ユーザフォロー = TLに流れる', async () => { @@ -2023,11 +2007,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); const bobRenote = await renote(bobNote.id, bob); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + }, waitForPushToTlOptions); }); test('[チャンネル外リノート] チャンネルフォロー + ユーザフォロー = TLに流れる', async () => { @@ -2041,11 +2025,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); const bobRenote = await renote(bobNote.id, bob); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + }, waitForPushToTlOptions); }); test('[チャンネル外リノート] チャンネル未フォロー + ユーザ未フォロー + チャンネルミュート = TLに流れない', async () => { @@ -2139,11 +2123,17 @@ describe('Timelines', () => { carolNote = await post(carol, { text: 'kon\'nichiwa' }); daveNote = await post(dave, { text: 'hello' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.some(note => note.id === daveNote.id), true); + }, waitForPushToTlOptions); await api('admin/suspend-user', { userId: carol.id }, root); await api('admin/suspend-user', { userId: dave.id }, root); - await setTimeout(250); }); test('凍結後に凍結されたユーザーのノートは見えなくなる', async () => { @@ -2158,7 +2148,6 @@ describe('Timelines', () => { test('凍結解除後に凍結されていたユーザーのノートは見えるようになる', async () => { await api('admin/unsuspend-user', { userId: carol.id }, root); await api('admin/unsuspend-user', { userId: dave.id }, root); - await setTimeout(250); const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); @@ -2186,11 +2175,15 @@ describe('Timelines', () => { carolNote = await post(carol, { text: 'kon\'nichiwa' }); elleNote = await post(elle, { text: 'hi there' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === elleNote.id), true); + }, waitForPushToTlOptions); await api('admin/suspend-user', { userId: carol.id }, root); await api('admin/suspend-user', { userId: elle.id }, root); - await setTimeout(250); }); test('凍結後に凍結されたユーザーのノートは見えなくなる', async () => { @@ -2204,7 +2197,6 @@ describe('Timelines', () => { test('凍結解除後に凍結されていたユーザーのノートは見えるようになる', async () => { await api('admin/unsuspend-user', { userId: carol.id }, root); await api('admin/unsuspend-user', { userId: elle.id }, root); - await setTimeout(250); const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); @@ -2221,14 +2213,13 @@ 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 setTimeout(250); const bobNote = await post(bob, { text: 'hi' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('リスインしているフォローしていないユーザーの visibility: home なノートが含まれる', async () => { @@ -2236,14 +2227,13 @@ 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 setTimeout(250); const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれない', async () => { @@ -2251,7 +2241,6 @@ 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 setTimeout(250); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); await waitForPushToTl(); @@ -2266,7 +2255,6 @@ 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 setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -2282,16 +2270,15 @@ 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 setTimeout(250); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + }, waitForPushToTlOptions); }); test('withReplies: false でリスインしているフォローしていないユーザーからの自分への返信が含まれる', async () => { @@ -2300,15 +2287,14 @@ 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 setTimeout(250); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('withReplies: false でリスインしているフォローしていないユーザーの他人への返信が含まれない', async () => { @@ -2317,7 +2303,6 @@ 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 setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -2334,15 +2319,14 @@ 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 setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('リスインしているフォローしているユーザーの visibility: home なノートが含まれる', async () => { @@ -2351,14 +2335,13 @@ 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 setTimeout(250); const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('リスインしているフォローしているユーザーの visibility: followers なノートが含まれる', async () => { @@ -2367,15 +2350,14 @@ 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 setTimeout(250); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + }, waitForPushToTlOptions); }); test('リスインしている自分の visibility: followers なノートが含まれる', async () => { @@ -2383,15 +2365,14 @@ 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 setTimeout(250); const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); + }, waitForPushToTlOptions); }); test('リスインしているユーザーのチャンネルノートが含まれない', async () => { @@ -2400,7 +2381,6 @@ 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 setTimeout(250); const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); await waitForPushToTl(); @@ -2419,12 +2399,12 @@ describe('Timelines', () => { const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { fileIds: [file.id] }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/user-list-timeline', { listId: list.id, withFiles: true }, alice); - const res = await api('notes/user-list-timeline', { listId: list.id, withFiles: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + }, waitForPushToTlOptions); }, 1000 * 10); test('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => { @@ -2432,15 +2412,14 @@ 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 setTimeout(250); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + }, waitForPushToTlOptions); }); test('リスインしているユーザーの自身宛てではない visibility: specified なノートが含まれない', async () => { @@ -2449,7 +2428,6 @@ 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 setTimeout(250); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); await waitForPushToTl(); @@ -2657,11 +2635,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); const bobRenote = await renote(bobNote.id, bob); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice); - const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + }, waitForPushToTlOptions); }); test('[チャンネル外リノート] チャンネルフォロー + リスインしてる = TLに流れる', async () => { @@ -2677,11 +2655,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); const bobRenote = await renote(bobNote.id, bob); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice); - const res = await api('notes/user-list-timeline', { limit: 100, listId: list.id }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + }, waitForPushToTlOptions); }); test('[チャンネル外リノート] チャンネル未フォロー + リスインしてない + チャンネルミュート = TLに流れない', async () => { @@ -2772,11 +2750,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'hi' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('users/notes', { userId: bob.id }, alice); - const res = await api('users/notes', { userId: bob.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('フォローしていないユーザーの visibility: followers なノートが含まれない', async () => { @@ -2795,15 +2773,14 @@ describe('Timelines', () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await setTimeout(250); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('users/notes', { userId: bob.id }, alice); - const res = await api('users/notes', { userId: bob.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + }, waitForPushToTlOptions); }); test('自身の visibility: followers なノートが含まれる', async () => { @@ -2811,12 +2788,12 @@ describe('Timelines', () => { const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('users/notes', { userId: alice.id }, alice); - const res = await api('users/notes', { userId: alice.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); + }, waitForPushToTlOptions); }); test('チャンネル投稿が含まれない', async () => { @@ -2841,12 +2818,12 @@ describe('Timelines', () => { const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('users/notes', { userId: bob.id }, alice); - const res = await api('users/notes', { userId: bob.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false); + }, waitForPushToTlOptions); }); test('[withReplies: true] 他人への返信が含まれる', async () => { @@ -2856,12 +2833,12 @@ describe('Timelines', () => { const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); - const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + }, waitForPushToTlOptions); }); test('[withReplies: true] 他人への visibility: specified な返信が含まれない', async () => { @@ -2871,12 +2848,12 @@ describe('Timelines', () => { const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); - const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false); + }, waitForPushToTlOptions); }); test('[withFiles: true] ファイル付きノートのみ含まれる', async () => { @@ -2886,12 +2863,12 @@ describe('Timelines', () => { const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { fileIds: [file.id] }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('users/notes', { userId: bob.id, withFiles: true }, alice); - const res = await api('users/notes', { userId: bob.id, withFiles: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + }, waitForPushToTlOptions); }, 1000 * 10); test('[withChannelNotes: true] チャンネル投稿が含まれる', async () => { @@ -2900,11 +2877,11 @@ describe('Timelines', () => { const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); - const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('[withChannelNotes: true] 他人が取得した場合センシティブチャンネル投稿が含まれない', async () => { @@ -2926,18 +2903,17 @@ describe('Timelines', () => { const channel = await api('channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body); const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, bob); - const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, bob); - - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('ミュートしているユーザーに関連する投稿が含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('mute/create', { userId: carol.id }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); @@ -2952,7 +2928,6 @@ describe('Timelines', () => { const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); await api('mute/create', { userId: carol.id }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id }); const bobNote = await post(bob, { renoteId: daveNote.id }); @@ -2969,7 +2944,6 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('mute/create', { userId: carol.id }, alice); - await setTimeout(250); const carolNote = await post(carol, { text: 'hi' }); const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id }); const bobNote = await post(bob, { renoteId: daveNote.id }); @@ -2985,22 +2959,21 @@ describe('Timelines', () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('mute/create', { userId: bob.id }, alice); - await setTimeout(250); 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 }); const bobNote4 = await post(bob, { renoteId: bobNote2.id }); const bobNote5 = await post(bob, { renoteId: bobNote3.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('users/notes', { userId: bob.id }, alice); - const res = await api('users/notes', { userId: bob.id }, alice); - - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote3.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote4.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote5.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote3.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote4.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote5.id), true); + }, waitForPushToTlOptions); }); test('自身の visibility: specified なノートが含まれる', async () => { @@ -3008,11 +2981,11 @@ describe('Timelines', () => { const aliceNote = await post(alice, { text: 'hi', visibility: 'specified' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('users/notes', { userId: alice.id, withReplies: true }, alice); - const res = await api('users/notes', { userId: alice.id, withReplies: true }, alice); - - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + }, waitForPushToTlOptions); }); test('visibleUserIds に指定されてない visibility: specified なノートが含まれない', async () => { @@ -3063,11 +3036,11 @@ describe('Timelines', () => { const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); - const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('チャンネルミュート = TLに流れない', async () => { @@ -3095,11 +3068,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); const bobRenote = await renote(bobNote.id, bob); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); - const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + }, waitForPushToTlOptions); }); test('[チャンネル外リノート] チャンネルミュート = TLに流れない', async () => { @@ -3130,12 +3103,12 @@ describe('Timelines', () => { const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('channels/timeline', { channelId: channel.id }, alice); - const res = await api('channels/timeline', { channelId: channel.id }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('閲覧中チャンネルとは別チャンネルのノートは含まれない', async() => { @@ -3164,11 +3137,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); const bobRenote = await post(bob, { channelId: channel.id, renoteId: bobNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('channels/timeline', { channelId: channel.id }, alice); - const res = await api('channels/timeline', { channelId: channel.id }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + }, waitForPushToTlOptions); }); test('閲覧中チャンネルとは別チャンネルからのリノートが含まれる', async() => { @@ -3181,11 +3154,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel2.id }); const bobRenote = await post(bob, { channelId: channel.id, renoteId: bobNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('channels/timeline', { channelId: channel.id }, alice); - const res = await api('channels/timeline', { channelId: channel.id }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + }, waitForPushToTlOptions); }); test('閲覧中チャンネルに自分の他人への返信が含まれる', async () => { @@ -3196,11 +3169,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); const aliceNote = await post(alice, { text: 'hi', replyId: bobNote.id, channelId: channel.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('channels/timeline', { channelId: channel.id }, alice); - const res = await api('channels/timeline', { channelId: channel.id }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + }, waitForPushToTlOptions); }); test('閲覧中チャンネルに他人の自分への返信が含まれる', async () => { @@ -3211,11 +3184,11 @@ describe('Timelines', () => { const aliceNote = await post(alice, { text: 'hi', channelId: channel.id }); const bobNote = await post(bob, { text: 'ok', replyId: aliceNote.id, channelId: channel.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('channels/timeline', { channelId: channel.id }, alice); - const res = await api('channels/timeline', { channelId: channel.id }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('閲覧中チャンネルにミュートしているユーザのノートは含まれない', async () => { @@ -3263,12 +3236,12 @@ describe('Timelines', () => { const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('channels/timeline', { channelId: channel.id }, alice); - const res = await api('channels/timeline', { channelId: channel.id }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }, waitForPushToTlOptions); }); test('閲覧中チャンネルをミュートしていても、同チャンネルのリノートが含まれる', async () => { @@ -3281,11 +3254,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); const bobRenote = await post(bob, { channelId: channel.id, renoteId: bobNote.id }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('channels/timeline', { channelId: channel.id }, alice); - const res = await api('channels/timeline', { channelId: channel.id }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + }, waitForPushToTlOptions); }); test('閲覧中チャンネルをミュートしていても、同チャンネルのリプライが含まれる', async () => { @@ -3298,11 +3271,11 @@ describe('Timelines', () => { const bobNote = await post(bob, { text: 'ok', channelId: channel.id }); const bobRenote = await post(bob, { channelId: channel.id, replyId: bobNote.id, text: 'ho' }); - await waitForPushToTl(); + await vi.waitFor(async () => { + const res = await api('channels/timeline', { channelId: channel.id }, alice); - const res = await api('channels/timeline', { channelId: channel.id }, alice); - - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + }, waitForPushToTlOptions); }); test('閲覧中チャンネルとは別チャンネルをミュートしているとき、そのチャンネルからのリノートは含まれない', async() => { diff --git a/packages/frontend-embed/src/components/EmNote.vue b/packages/frontend-embed/src/components/EmNote.vue index f5b064c293..c8bea02135 100644 --- a/packages/frontend-embed/src/components/EmNote.vue +++ b/packages/frontend-embed/src/components/EmNote.vue @@ -149,7 +149,7 @@ const isRenote = Misskey.note.isPureRenote(note.value); const rootEl = shallowRef(); const renoteTime = shallowRef(); -const appearNote = computed(() => getAppearNote(note.value)); +const appearNote = computed(() => getAppearNote(note.value) ?? note.value); const showContent = ref(false); const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); const isLong = shouldCollapsed(appearNote.value, []); diff --git a/packages/frontend-embed/src/components/EmPagination.vue b/packages/frontend-embed/src/components/EmPagination.vue index bd49d127a9..197584908c 100644 --- a/packages/frontend-embed/src/components/EmPagination.vue +++ b/packages/frontend-embed/src/components/EmPagination.vue @@ -244,7 +244,7 @@ const fetchMore = async (): Promise => { if (i === 10) item._shouldInsertAd_ = true; } - const reverseConcat = _res => { + const reverseConcat = (_res: MisskeyEntity[]) => { const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight(); const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY; diff --git a/packages/frontend-embed/src/components/EmReactionsViewer.vue b/packages/frontend-embed/src/components/EmReactionsViewer.vue index f5aa6bdc3f..91a5cd0fdd 100644 --- a/packages/frontend-embed/src/components/EmReactionsViewer.vue +++ b/packages/frontend-embed/src/components/EmReactionsViewer.vue @@ -27,8 +27,8 @@ const initialReactions = new Set(Object.keys(props.note.reactions)); const reactions = ref<[string, number][]>([]); const hasMoreReactions = ref(false); -if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.myReaction)) { - reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction]; +if (props.note.myReaction != null && !(props.note.myReaction in props.note.reactions)) { + reactions.value.push([props.note.myReaction, props.note.reactions[props.note.myReaction]]); } function onMockToggleReaction(emoji: string, count: number) { diff --git a/packages/frontend-embed/src/components/I18n.vue b/packages/frontend-embed/src/components/I18n.vue index 9866e50958..9bd699d5ac 100644 --- a/packages/frontend-embed/src/components/I18n.vue +++ b/packages/frontend-embed/src/components/I18n.vue @@ -46,6 +46,9 @@ const parsed = computed(() => { }); const render = () => { - return h(props.tag, parsed.value.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]())); + // slotsの型はTの条件型で決まり文字列インデックスアクセスができないため、 + // frontend側の同名コンポーネント (packages/frontend/src/components/global/I18n.vue) と同じくanyでキャストする + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return h(props.tag, parsed.value.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : (slots as any)[x.arg]())); }; diff --git a/packages/frontend-embed/src/custom-emojis.ts b/packages/frontend-embed/src/custom-emojis.ts index d5b40885c1..e66a6b3112 100644 --- a/packages/frontend-embed/src/custom-emojis.ts +++ b/packages/frontend-embed/src/custom-emojis.ts @@ -45,11 +45,11 @@ export async function fetchCustomEmojis(force = false) { set('lastEmojisFetchedAt', now); } -let cachedTags; +let cachedTags: string[] | null = null; export function getCustomEmojiTags() { if (cachedTags) return cachedTags; - const tags = new Set(); + const tags = new Set(); for (const emoji of customEmojis.value) { for (const tag of emoji.aliases) { tags.add(tag); diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 97b89cb135..32ced9eaf2 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -85,8 +85,6 @@ "@storybook/core-events": "8.6.18", "@storybook/manager-api": "8.6.18", "@storybook/preview-api": "8.6.18", - "@storybook/react": "10.4.6", - "@storybook/react-vite": "10.4.6", "@storybook/test": "8.6.18", "@storybook/theming": "8.6.18", "@storybook/types": "8.6.18", @@ -110,10 +108,7 @@ "@typescript-eslint/parser": "8.61.1", "@vitest/coverage-v8": "4.1.9", "@vue/compiler-core": "3.5.38", - "acorn": "8.17.0", "astring": "1.9.0", - "cross-env": "10.1.0", - "cypress": "15.17.0", "eslint-plugin-import": "2.32.0", "eslint-plugin-vue": "10.9.2", "happy-dom": "20.10.6", @@ -132,7 +127,6 @@ "rollup-plugin-visualizer": "7.0.1", "sass-embedded": "1.100.0", "seedrandom": "3.0.5", - "start-server-and-test": "3.0.11", "storybook": "10.4.6", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "tsx": "4.22.4", diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index e8c93b7092..47cce01de8 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only