diff --git a/.claude/commands/harness-audit.md b/.claude/commands/harness-audit.md index ce1d62ba8c..7268503859 100644 --- a/.claude/commands/harness-audit.md +++ b/.claude/commands/harness-audit.md @@ -35,7 +35,7 @@ Misskey リポジトリの `.claude/` 構成を 7 カテゴリで採点し、改 | 2 | Context Efficiency | frontmatter description の冗長度、SKILL.md の長さ分布、重複情報、CLAUDE.md の肥大化 | | 3 | Quality Gates | Stop / PreToolUse / PostToolUse hook の整備、`/quality-gate` 等の完了前ゲートの有無、自動 lint/typecheck | | 4 | Memory Persistence | `.claude/skills/*/SKILL.md` と `references/` の同期状態を評価。プロジェクト側 `.claude/memory/` は未採用方針 (auto-memory はユーザーホーム側で自動運用) のため、ここを採点起点にせず既定 5/10 から開始する | -| 5 | Eval Coverage | `working-on-backend` / `working-on-frontend` の testing リファレンス (backend-testing.md / frontend-testing.md) の網羅、Misskey 固有の e2e/fed/Storybook/Cypress 適用ガイド | +| 5 | Eval Coverage | `working-on-backend` / `working-on-frontend` の testing リファレンス (backend-testing.md / frontend-testing.md) の網羅、Misskey 固有の e2e/fed/Storybook/Playwright 適用ガイド | | 6 | Security Guardrails | SPDX 規約適用、migration 不変性ルール、ja-JP.yml 限定編集ルール、secrets 検出 | | 7 | Cost Efficiency | enabledPlugins の重複・過剰、context-budget の整備、MCP 過剰登録なし | diff --git a/.claude/commands/quality-gate.md b/.claude/commands/quality-gate.md index 6844c2bcc0..720ab56fa4 100644 --- a/.claude/commands/quality-gate.md +++ b/.claude/commands/quality-gate.md @@ -21,7 +21,7 @@ note: 元 ECC 版は言語自動判定 + format/lint/type のジェネリック `/quality-gate [scope]` -完了前の **軽量** 品質チェック。重い E2E / 連合テスト (test:e2e / test:fed / Cypress) は CI 側で実行されるため、本コマンドには含めない。 +完了前の **軽量** 品質チェック。重い E2E / 連合テスト (test:e2e / test:fed / Playwright) は CI 側で実行されるため、本コマンドには含めない。 ## Scope @@ -120,4 +120,4 @@ Frontend ut: PASS (87/87) - ジェネリックな言語自動判定を排除し、Misskey 固定 pipeline に。 - formatter フェーズなし (Misskey は ESLint --fix のみ採用)。 -- e2e / federation / Cypress は重いため除外し CI 側に委譲。 +- e2e / federation / Playwright は重いため除外し CI 側に委譲。 diff --git a/.claude/skills/working-on-backend/references/knowledge/backend-testing.md b/.claude/skills/working-on-backend/references/knowledge/backend-testing.md index 10add7d84c..8fd6dfd9c4 100644 --- a/.claude/skills/working-on-backend/references/knowledge/backend-testing.md +++ b/.claude/skills/working-on-backend/references/knowledge/backend-testing.md @@ -26,7 +26,7 @@ cp .github/misskey/test.yml .config/test.yml 補足: -- ルートの `pnpm start:test` (Cypress 用にテストサーバーを起動するコマンド) を使う経路では実行時に `ncp` で自動コピーされる ([package.json](../../../../../package.json))。それ以外で backend テストを直接走らせる時は上記の手動コピーが必要 +- ルートの `pnpm start:test` (Playwright 用にテストサーバーを起動するコマンド) を使う経路では実行時に `ncp` で自動コピーされる ([package.json](../../../../../package.json))。それ以外で backend テストを直接走らせる時は上記の手動コピーが必要 - すでに `.config/test.yml` があれば各テストスクリプトの内部 `compile-config` で十分なので、追加で `pnpm --filter backend compile-config` を叩く必要はない - `pnpm start:test` は backend e2e テスト (`pnpm --filter backend test:e2e`) の前提ではない (ポート競合の元になるため使わないこと) diff --git a/.claude/skills/working-on-frontend/SKILL.md b/.claude/skills/working-on-frontend/SKILL.md index a4bcab53b4..3fd04257e0 100644 --- a/.claude/skills/working-on-frontend/SKILL.md +++ b/.claude/skills/working-on-frontend/SKILL.md @@ -27,7 +27,7 @@ SKILL.md 本体は references への索引だけ。具体的な手順や規約 - SCSS Modules / `--MI_THEME-*` `--MI-*` CSS 変数 / グローバル utility class (`_button` 等) → [references/knowledge/scss-modules.md](references/knowledge/scss-modules.md) - `os.alert` / `os.confirm` / `os.popup` 等 UI ヘルパー (ブラウザ標準 `alert()` 直呼びは禁止) → [references/knowledge/os-api.md](references/knowledge/os-api.md) - `*.stories.impl.ts` 併設規則 + 複数 story / argTypes / layout / action パターン → [references/knowledge/storybook.md](references/knowledge/storybook.md) -- frontend Vitest / Cypress E2E の書き方と前提 → [references/knowledge/frontend-testing.md](references/knowledge/frontend-testing.md) +- frontend Vitest / Playwright E2E の書き方と前提 → [references/knowledge/frontend-testing.md](references/knowledge/frontend-testing.md) ## 必ず最後に通る場所 diff --git a/.claude/skills/working-on-frontend/references/knowledge/frontend-testing.md b/.claude/skills/working-on-frontend/references/knowledge/frontend-testing.md index cfd81b8f92..49fe0fa59c 100644 --- a/.claude/skills/working-on-frontend/references/knowledge/frontend-testing.md +++ b/.claude/skills/working-on-frontend/references/knowledge/frontend-testing.md @@ -1,4 +1,4 @@ -# Frontend テスト (Vitest / Cypress) +# Frontend テスト (Vitest / Playwright) Misskey frontend のテスト構成。 @@ -13,11 +13,11 @@ pnpm --filter frontend test-and-coverage # カバレッジ付き - 主な配置: `packages/frontend/test/*.test.ts` (例: `i18n.test.ts`, `theme.test.ts`, `is-birthday.test.ts`) - ビルドツール周りなど対象コードと隣接させた方が分かりやすいテストは、コードと同じディレクトリに `*.test.ts` として置く (例: [packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts](../../../../../packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts)) -- 共有コンポーネント (`MkX.vue`) のユニットテストは現状少なく、`*.spec.ts` / `__tests__/` 形式は採用していない (Storybook + Cypress でカバー) +- 共有コンポーネント (`MkX.vue`) のユニットテストは現状少なく、`*.spec.ts` / `__tests__/` 形式は採用していない (Storybook + Playwright でカバー) -## Cypress E2E +## Playwright E2E -Cypress は **起動済みのテストサーバー** に対して走るため、unit より前提が多い。[.github/workflows/test-frontend.yml](../../../../../.github/workflows/test-frontend.yml) の `e2e` ジョブと同じ手順をローカルで踏む: +Playwright は **起動済みのテストサーバー** に対して走るため、unit より前提が多い。[.github/workflows/test-frontend.yml](../../../../../.github/workflows/test-frontend.yml) の `e2e` ジョブと同じ手順をローカルで踏む: ```bash # 1. テスト用 DB / Redis を起動 (テスト用ポート。開発用の compose.local-db.yml ではない) @@ -29,15 +29,14 @@ cp .github/misskey/test.yml .config/test.yml # 3. 全体ビルド pnpm build -# 4. テストサーバー起動 + Cypress 実行 (いずれもルートから) -pnpm e2e # 内部で pnpm start:test を起動し http://localhost:61812 を待って Cypress run -pnpm cy:open # 対話的に開く (サーバーは別途 pnpm start:test で起動しておく) +# 4. テストサーバー起動 + Playwright 実行 (いずれもルートから) +pnpm e2e # 内部で pnpm start:test を起動し http://localhost:61812 を待って Playwright を実行 ``` -- 設定: ルート [cypress.config.ts](../../../../../cypress.config.ts) -- テスト本体は [cypress/](../../../../../cypress/) 配下 +- 設定: ルート [packages/frontend/playwright.config.ts](../../../../../packages/frontend/playwright.config.ts) で、`packages/frontend/test/e2e/` 配下の `*.spec.ts` を対象にする +- テスト本体は [packages/frontend/test/e2e/](../../../../../packages/frontend/test/e2e/) 配下に配置する。 -新規 frontend 機能の E2E は Cypress に書くのが基本。ただし対象は主要 UI フロー (login / post / drive etc) に限定し、細かい単位テストは Vitest または Storybook で代替する慣習。 +新規 frontend 機能の E2E は Playwright に書くのが基本。ただし対象は主要 UI フロー (login / post / drive etc) に限定し、細かい単位テストは Vitest または Storybook で代替する慣習。 ## Storybook (視覚確認 + Chromatic 視覚回帰) @@ -55,6 +54,6 @@ pnpm --filter frontend build-storybook # 静的ビルド frontend のテスト種別で DB / Redis の要否が違う: - **Vitest (unit)** — DB 不要。ロジック / コンポーネント単体のテストで backend に繋がない (CI の `vitest` ジョブにも `services:` は無い) -- **Cypress (E2E)** — テストサーバー (`pnpm start:test`) 経由で backend に繋ぐため DB / Redis が必要。**テスト用ポートの [packages/backend/test/compose.yml](../../../../../packages/backend/test/compose.yml)** を使う (上記 Cypress E2E の手順を参照) +- **Playwright (E2E)** — テストサーバー (`pnpm start:test`) 経由で backend に繋ぐため DB / Redis が必要。**テスト用ポートの [packages/backend/test/compose.yml](../../../../../packages/backend/test/compose.yml)** を使う (上記 Playwright E2E の手順を参照) 開発用の `compose.local-db.yml` (db `5432` / redis `6379`) は **テストには使わない**。テスト用の `packages/backend/test/compose.yml` (`54312` / `56312`) とはポートが異なり、混同すると接続できない。 diff --git a/.config/cypress-devcontainer.yml b/.config/playwright-devcontainer.yml similarity index 100% rename from .config/cypress-devcontainer.yml rename to .config/playwright-devcontainer.yml diff --git a/.devcontainer/init.sh b/.devcontainer/init.sh index 216292b082..12d622581b 100755 --- a/.devcontainer/init.sh +++ b/.devcontainer/init.sh @@ -12,4 +12,4 @@ pnpm install --frozen-lockfile cp .devcontainer/devcontainer.yml .config/default.yml pnpm build pnpm migrate -pnpm exec cypress install +pnpm --filter frontend exec playwright install --with-deps chromium diff --git a/.github/labeler.yml b/.github/labeler.yml index b64d726d65..78341fd6cb 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -16,7 +16,7 @@ 'packages/frontend:test': - any: - changed-files: - - any-glob-to-any-file: ['cypress/**/*'] + - any-glob-to-any-file: ['packages/frontend/test/**/*'] 'packages/sw': - any: diff --git a/.github/workflows/check-spdx-license-id.yml b/.github/workflows/check-spdx-license-id.yml index f75db131f0..d0cf6e3cf6 100644 --- a/.github/workflows/check-spdx-license-id.yml +++ b/.github/workflows/check-spdx-license-id.yml @@ -44,7 +44,6 @@ jobs: } directories=( - "cypress/e2e" "packages/backend/migration" "packages/backend/src" "packages/backend/test" diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index 6e55760462..e8a98b2b78 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -57,11 +57,6 @@ jobs: name: E2E tests (frontend) runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - browser: [chrome] - services: postgres: image: postgres:18 @@ -79,12 +74,6 @@ jobs: - uses: actions/checkout@v6.0.3 with: submodules: true - # https://github.com/cypress-io/cypress-docker-images/issues/150 - #- name: Install mplayer for FireFox - # run: sudo apt install mplayer -y - # if: ${{ matrix.browser == 'firefox' }} - #- uses: browser-actions/setup-firefox@latest - # if: ${{ matrix.browser == 'firefox' }} - name: Setup pnpm uses: pnpm/action-setup@v6.0.9 - name: Use Node.js @@ -97,29 +86,14 @@ jobs: run: cp .github/misskey/test.yml .config - name: Build run: pnpm build - # https://github.com/cypress-io/cypress/issues/4351#issuecomment-559489091 - - name: ALSA Env - run: echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc - # XXX: This tries reinstalling Cypress if the binary is not cached - # Remove this when the cache issue is fixed - - name: Cypress install - run: pnpm exec cypress install - - name: Cypress run - uses: cypress-io/github-action@v7.4.0 + - name: Playwright install + working-directory: packages/frontend + run: pnpm exec playwright install --with-deps chromium + - name: Test + run: pnpm start-server-and-test start:test http://localhost:61812 "pnpm --filter frontend test:e2e" timeout-minutes: 15 - with: - install: false - start: pnpm start:test - wait-on: 'http://localhost:61812' - headed: true - browser: ${{ matrix.browser }} - uses: actions/upload-artifact@v7 if: failure() with: - name: ${{ matrix.browser }}-cypress-screenshots - path: cypress/screenshots - - uses: actions/upload-artifact@v7 - if: always() - with: - name: ${{ matrix.browser }}-cypress-videos - path: cypress/videos + name: playwright-e2e-artifacts + path: packages/frontend/test/e2e/artifacts diff --git a/.gitignore b/.gitignore index ed1daaf489..e975691b93 100644 --- a/.gitignore +++ b/.gitignore @@ -23,9 +23,8 @@ packages/sw/.yarn/cache # pnpm .pnpm-store -# Cypress -cypress/screenshots -cypress/videos +# Playwright +packages/frontend/test/e2e/artifacts # Coverage coverage @@ -35,7 +34,7 @@ coverage !/.config/example.yml !/.config/docker_example.yml !/.config/docker_example.env -!/.config/cypress-devcontainer.yml +!/.config/playwright-devcontainer.yml docker-compose.yml ./compose.yml .devcontainer/compose.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index be5e129d34..1b5e64eed4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Fix: デバイスタイプをスマートフォンに固定している状態で画面幅が広いとき、画面左上のアイコンが表示されない問題を修正 - Fix: チャットでIMEの変換を確定するEnterでメッセージが送信されてしまうことがある問題を修正 - Fix: 自分へのメンションに対する色分けで、判定が大文字/小文字を区別していた問題を修正 +- Fix: いくつかのイベントリスナーが正しく解除されない問題を修正(メモリ使用量の改善) ### Server - Enhance: センシティブメディアの判定を外部サービス ([sensitive-detector](https://github.com/misskey-dev/sensitive-detector)) に分離し、`nsfwjs` / `@tensorflow/tfjs(-node)` の同梱と NSFW 判定モデルを廃止 (#16804) diff --git a/cypress.config.ts b/cypress.config.ts deleted file mode 100644 index 361acaf6e5..0000000000 --- a/cypress.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'cypress' - -export default defineConfig({ - e2e: { - baseUrl: 'http://localhost:61812', - }, -}) diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.ts deleted file mode 100644 index 4f2f700146..0000000000 --- a/cypress/e2e/basic.cy.ts +++ /dev/null @@ -1,265 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -describe('Before setup instance', () => { - beforeEach(() => { - cy.resetState(); - }); - - afterEach(() => { - // テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。 - // waitを入れることでそれを防止できる - cy.wait(1000); - }); - - it('successfully loads', () => { - cy.visitHome(); - }); - - it('setup instance', () => { - cy.visitHome(); - - cy.intercept('POST', '/api/admin/accounts/create').as('signup'); - - cy.get('[data-cy-admin-initial-password] input').type('example_password_please_change_this_or_you_will_get_hacked'); - cy.get('[data-cy-admin-username] input').type('admin'); - cy.get('[data-cy-admin-password] input').type('admin1234'); - cy.get('[data-cy-admin-ok]').click(); - - // なぜか動かない - //cy.wait('@signup').should('have.property', 'response.statusCode'); - cy.wait('@signup'); - - cy.intercept('POST', '/api/admin/update-meta').as('update-meta'); - - cy.get('[data-cy-next]').click(); - cy.get('[data-cy-server-name] input').type('Testskey'); - cy.get('[data-cy-server-setup-wizard-apply]').click(); - - cy.wait('@update-meta'); - }); -}); - -describe('After setup instance', () => { - beforeEach(() => { - cy.resetState(); - - // インスタンス初期セットアップ - cy.registerUser('admin', 'pass', true); - }); - - afterEach(() => { - // テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。 - // waitを入れることでそれを防止できる - cy.wait(1000); - }); - - it('successfully loads', () => { - cy.visitHome(); - }); - - it('signup', () => { - cy.visitHome(); - - cy.intercept('POST', '/api/signup').as('signup'); - - cy.get('[data-cy-signup]').click(); - cy.get('[data-cy-signup-rules-continue]').should('be.disabled'); - cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click(); - cy.get('[data-cy-modal-dialog-ok]').click(); - cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled'); - cy.get('[data-cy-signup-rules-continue]').click(); - - cy.get('[data-cy-signup-submit]').should('be.disabled'); - cy.get('[data-cy-signup-username] input').type('alice'); - cy.get('[data-cy-signup-submit]').should('be.disabled'); - cy.get('[data-cy-signup-password] input').type('alice1234'); - cy.get('[data-cy-signup-submit]').should('be.disabled'); - cy.get('[data-cy-signup-password-retype] input').type('alice1234'); - cy.get('[data-cy-signup-submit]').should('be.disabled'); - cy.get('[data-cy-signup-invitation-code] input').type('test-invitation-code'); - cy.get('[data-cy-signup-submit]').should('not.be.disabled'); - cy.get('[data-cy-signup-submit]').click(); - - cy.wait('@signup'); - }); - - it('signup with duplicated username', () => { - cy.registerUser('alice', 'alice1234'); - - cy.visitHome(); - - // ユーザー名が重複している場合の挙動確認 - cy.get('[data-cy-signup]').click(); - cy.get('[data-cy-signup-rules-continue]').should('be.disabled'); - cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click(); - cy.get('[data-cy-modal-dialog-ok]').click(); - cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled'); - cy.get('[data-cy-signup-rules-continue]').click(); - - cy.get('[data-cy-signup-username] input').type('alice'); - cy.get('[data-cy-signup-password] input').type('alice1234'); - cy.get('[data-cy-signup-password-retype] input').type('alice1234'); - cy.get('[data-cy-signup-submit]').should('be.disabled'); - }); -}); - -describe('After user signup', () => { - beforeEach(() => { - cy.resetState(); - - // インスタンス初期セットアップ - cy.registerUser('admin', 'pass', true); - - // ユーザー作成 - cy.registerUser('alice', 'alice1234'); - }); - - afterEach(() => { - // テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。 - // waitを入れることでそれを防止できる - cy.wait(1000); - }); - - it('successfully loads', () => { - cy.visitHome(); - }); - - it('signin', () => { - cy.visitHome(); - - cy.intercept('POST', '/api/signin-flow').as('signin'); - - cy.get('[data-cy-signin]').click(); - - cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 }); - // Enterキーで続行できるかの確認も兼ねる - cy.get('[data-cy-signin-username] input').type('alice{enter}'); - - cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 }); - // Enterキーで続行できるかの確認も兼ねる - cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); - - cy.wait('@signin'); - }); - - it('suspend', function() { - cy.request('POST', '/api/admin/suspend-user', { - i: this.admin.token, - userId: this.alice.id, - }); - - cy.visitHome(); - - cy.get('[data-cy-signin]').click(); - - cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 }); - cy.get('[data-cy-signin-username] input').type('alice{enter}'); - - // TODO: cypressにブラウザの言語指定できる機能が実装され次第英語のみテストするようにする - cy.contains(/アカウントが凍結されています|This account has been suspended due to/gi); - }); -}); - -describe('After user signed in', () => { - beforeEach(() => { - cy.resetState(); - - // インスタンス初期セットアップ - cy.registerUser('admin', 'pass', true); - - // ユーザー作成 - cy.registerUser('alice', 'alice1234'); - - cy.login('alice', 'alice1234'); - }); - - afterEach(() => { - // テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。 - // waitを入れることでそれを防止できる - cy.wait(1000); - }); - - it('successfully loads', () => { - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).should('be.visible'); - }); - - it('account setup wizard', () => { - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).click(); - - cy.get('[data-cy-user-setup-user-name] input').type('ありす'); - cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ'); - // TODO: アイコン設定テスト - - cy.get('[data-cy-user-setup-continue]').click(); - - // プライバシー設定 - - cy.get('[data-cy-user-setup-continue]').click(); - - // フォローはスキップ - - cy.get('[data-cy-user-setup-continue]').click(); - - // プッシュ通知設定はスキップ - - cy.get('[data-cy-user-setup-continue]').click(); - - cy.get('[data-cy-user-setup-continue]').click(); - }); -}); - -describe('After user setup', () => { - beforeEach(() => { - cy.resetState(); - - // インスタンス初期セットアップ - cy.registerUser('admin', 'pass', true); - - // ユーザー作成 - cy.registerUser('alice', 'alice1234'); - - cy.login('alice', 'alice1234'); - - // アカウント初期設定ウィザード - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click(); - cy.get('[data-cy-modal-dialog-ok]').click(); - }); - - afterEach(() => { - // テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。 - // waitを入れることでそれを防止できる - cy.wait(1000); - }); - - it('note', () => { - cy.get('[data-cy-open-post-form]').should('be.visible'); - cy.get('[data-cy-open-post-form]').click(); - cy.get('[data-cy-post-form-text]').type('Hello, Misskey!'); - cy.get('[data-cy-open-post-form-submit]').click(); - - cy.contains('Hello, Misskey!', { timeout: 15000 }); - }); - - it('open note form with hotkey', () => { - // Wait until the page loads - cy.get('[data-cy-open-post-form]').should('be.visible'); - // Use trigger() to give different `code` to test if hotkeys also work on non-QWERTY keyboards. - cy.document().trigger("keydown", { eventConstructor: 'KeyboardEvent', key: "n", code: "KeyL" }); - // See if the form is opened - cy.get('[data-cy-post-form-text]').should('be.visible'); - // Close it - cy.focused().trigger("keydown", { eventConstructor: 'KeyboardEvent', key: "Escape", code: "Escape" }); - // See if the form is closed - cy.get('[data-cy-post-form-text]').should('not.be.visible'); - }); -}); - -// TODO: 投稿フォームの公開範囲指定のテスト -// TODO: 投稿フォームのファイル添付のテスト -// TODO: 投稿フォームのハッシュタグ保持フィールドのテスト diff --git a/cypress/e2e/router.cy.ts b/cypress/e2e/router.cy.ts deleted file mode 100644 index 8d8fb3af31..0000000000 --- a/cypress/e2e/router.cy.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -describe('Router transition', () => { - describe('Redirect', () => { - // サーバの初期化。ルートのテストに関しては各describeごとに1度だけ実行で十分だと思う(使いまわした方が早い) - before(() => { - cy.resetState(); - - // インスタンス初期セットアップ - cy.registerUser('admin', 'pass', true); - - // ユーザー作成 - cy.registerUser('alice', 'alice1234'); - - cy.login('alice', 'alice1234'); - - // アカウント初期設定ウィザード - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click(); - cy.wait(500); - cy.get('[data-cy-modal-dialog-ok]').click(); - }); - - it('redirect to user profile', () => { - // テストのためだけに用意されたリダイレクト用ルートに飛ぶ - cy.visit('/redirect-test'); - - // プロフィールページのURLであることを確認する - cy.url().should('include', '/@alice') - }); - }); -}); diff --git a/cypress/e2e/widgets.cy.ts b/cypress/e2e/widgets.cy.ts deleted file mode 100644 index 847801a69f..0000000000 --- a/cypress/e2e/widgets.cy.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* flaky -describe('After user signed in', () => { - beforeEach(() => { - cy.resetState(); - cy.viewport('macbook-16'); - - // インスタンス初期セットアップ - cy.registerUser('admin', 'pass', true); - - // ユーザー作成 - cy.registerUser('alice', 'alice1234'); - - cy.login('alice', 'alice1234'); - - // アカウント初期設定ウィザード - cy.get('[data-cy-user-setup] [data-cy-modal-window-close]').click(); - cy.get('[data-cy-modal-dialog-ok]').click(); - }); - - afterEach(() => { - // テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。 - // waitを入れることでそれを防止できる - cy.wait(1000); - }); - - it('widget edit toggle is visible', () => { - cy.get('[data-cy-widget-edit]').should('be.visible'); - }); - - it('widget select should be visible in edit mode', () => { - cy.get('[data-cy-widget-edit]').click(); - cy.get('[data-cy-widget-select]').should('be.visible'); - }); - - it('first widget should be removed', () => { - cy.get('[data-cy-widget-edit]').click(); - cy.get('[data-cy-customize-container]:first-child [data-cy-customize-container-remove]._button').click(); - cy.get('[data-cy-customize-container]').should('have.length', 2); - }); - - function buildWidgetTest(widgetName) { - it(`${widgetName} widget should get added`, () => { - cy.get('[data-cy-widget-edit]').click(); - cy.get('[data-cy-widget-select] select').select(widgetName, { force: true }); - cy.get('[data-cy-bg]._modalBg[data-cy-transparent]').click({ multiple: true, force: true }); - cy.get('[data-cy-widget-add]').click({ force: true }); - cy.get(`[data-cy-mkw-${widgetName}]`).should('exist'); - }); - } - - buildWidgetTest('memo'); - buildWidgetTest('notifications'); - buildWidgetTest('timeline'); - buildWidgetTest('calendar'); - buildWidgetTest('rss'); - buildWidgetTest('trends'); - buildWidgetTest('clock'); - buildWidgetTest('activity'); - buildWidgetTest('photos'); - buildWidgetTest('digitalClock'); - buildWidgetTest('federation'); - buildWidgetTest('postForm'); - buildWidgetTest('slideshow'); - buildWidgetTest('serverMetric'); - buildWidgetTest('onlineUsers'); - buildWidgetTest('jobQueue'); - buildWidgetTest('button'); - buildWidgetTest('aiscript'); - buildWidgetTest('aichan'); -}); -*/ diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json deleted file mode 100644 index 02e4254378..0000000000 --- a/cypress/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts deleted file mode 100644 index 197ff963ac..0000000000 --- a/cypress/support/commands.ts +++ /dev/null @@ -1,67 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add('login', (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) - -Cypress.Commands.add('visitHome', () => { - cy.visit('/'); - cy.get('button', { timeout: 30000 }).should('be.visible'); -}) - -Cypress.Commands.add('resetState', () => { - // iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。 - // see https://github.com/misskey-dev/misskey/issues/13605#issuecomment-2053652123 - /* - cy.window().then(win => { - win.indexedDB.deleteDatabase('keyval-store'); - }); - */ - cy.request('POST', '/api/reset-db', {}).as('reset'); - cy.get('@reset').its('status').should('equal', 204); - cy.reload(true); -}); - -Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => { - const route = isAdmin ? '/api/admin/accounts/create' : '/api/signup'; - - cy.request('POST', route, { - username: username, - password: password, - ...(isAdmin ? { setupPassword: 'example_password_please_change_this_or_you_will_get_hacked' } : {}), - }).its('body').as(username); -}); - -Cypress.Commands.add('login', (username, password) => { - cy.visitHome(); - - cy.intercept('POST', '/api/signin-flow').as('signin'); - - cy.get('[data-cy-signin]').click(); - cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 }); - cy.get('[data-cy-signin-username] input').type(`${username}{enter}`); - cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 }); - cy.get('[data-cy-signin-password] input').type(`${password}{enter}`); - - cy.wait('@signin').as('signedIn'); -}); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts deleted file mode 100644 index 827a326d18..0000000000 --- a/cypress/support/e2e.ts +++ /dev/null @@ -1,34 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands' - -// Alternatively you can use CommonJS syntax: -// require('./commands') - -Cypress.on('uncaught:exception', (err, runnable) => { - if ([ - 'The source image cannot be decoded', - - // Chrome - 'ResizeObserver loop limit exceeded', - - // Firefox - 'ResizeObserver loop completed with undelivered notifications', - ].some(msg => err.message.includes(msg))) { - return false; - } -}); diff --git a/cypress/support/index.ts b/cypress/support/index.ts deleted file mode 100644 index c1bed21979..0000000000 --- a/cypress/support/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -declare global { - namespace Cypress { - interface Chainable { - login(username: string, password: string): Chainable; - - registerUser( - username: string, - password: string, - isAdmin?: boolean - ): Chainable; - - resetState(): Chainable; - - visitHome(): Chainable; - } - } -} - -export {} diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json deleted file mode 100644 index b91e34dc12..0000000000 --- a/cypress/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "lib": ["dom"], - "target": "esnext", - "types": ["cypress", "node"] - }, - "include": ["./**/*.ts"] -} diff --git a/package.json b/package.json index f710f031dd..c90030f3a5 100644 --- a/package.json +++ b/package.json @@ -41,10 +41,8 @@ "dev": "node scripts/dev.mjs", "check-dts": "node scripts/check-dts.mjs", "lint": "pnpm --no-bail -r lint && pnpm check-dts", - "cy:open": "pnpm cypress open --config-file=cypress.config.ts", - "cy:run": "pnpm cypress run", - "e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run", - "e2e-dev-container": "ncp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run", + "e2e": "pnpm start-server-and-test start:test http://localhost:61812 \"pnpm --filter frontend test:e2e\"", + "e2e-dev-container": "ncp ./.config/playwright-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 \"pnpm --filter frontend test:e2e\"", "backend-unit-test": "cd packages/backend && pnpm test", "backend-unit-test-and-coverage": "cd packages/backend && pnpm test-and-coverage", "test": "pnpm -r test", @@ -68,7 +66,6 @@ "@typescript-eslint/parser": "8.62.0", "@typescript/native-preview": "7.0.0-dev.20260426.1", "cross-env": "10.1.0", - "cypress": "15.18.0", "eslint": "9.39.4", "globals": "17.7.0", "ncp": "2.0.0", diff --git a/packages/frontend/assets/artist_palette_3d.png b/packages/frontend/assets/artist_palette_3d.png deleted file mode 100644 index e815352ece..0000000000 Binary files a/packages/frontend/assets/artist_palette_3d.png and /dev/null differ diff --git a/packages/frontend/assets/bell_3d.png b/packages/frontend/assets/bell_3d.png deleted file mode 100644 index 2598cdd82b..0000000000 Binary files a/packages/frontend/assets/bell_3d.png and /dev/null differ diff --git a/packages/frontend/assets/cloud_3d.png b/packages/frontend/assets/cloud_3d.png deleted file mode 100644 index a3a1de12dd..0000000000 Binary files a/packages/frontend/assets/cloud_3d.png and /dev/null differ diff --git a/packages/frontend/assets/desktop_computer_3d.png b/packages/frontend/assets/desktop_computer_3d.png deleted file mode 100644 index 85e92a02c0..0000000000 Binary files a/packages/frontend/assets/desktop_computer_3d.png and /dev/null differ diff --git a/packages/frontend/assets/electric_plug_3d.png b/packages/frontend/assets/electric_plug_3d.png deleted file mode 100644 index 431ef68c85..0000000000 Binary files a/packages/frontend/assets/electric_plug_3d.png and /dev/null differ diff --git a/packages/frontend/assets/gear_3d.png b/packages/frontend/assets/gear_3d.png deleted file mode 100644 index 050340b76c..0000000000 Binary files a/packages/frontend/assets/gear_3d.png and /dev/null differ diff --git a/packages/frontend/assets/link_3d.png b/packages/frontend/assets/link_3d.png deleted file mode 100644 index b1cb23080a..0000000000 Binary files a/packages/frontend/assets/link_3d.png and /dev/null differ diff --git a/packages/frontend/assets/locked_with_key_3d.png b/packages/frontend/assets/locked_with_key_3d.png deleted file mode 100644 index aae99b982d..0000000000 Binary files a/packages/frontend/assets/locked_with_key_3d.png and /dev/null differ diff --git a/packages/frontend/assets/mens_room_3d.png b/packages/frontend/assets/mens_room_3d.png deleted file mode 100644 index 8b85ca8782..0000000000 Binary files a/packages/frontend/assets/mens_room_3d.png and /dev/null differ diff --git a/packages/frontend/assets/musical_note_3d.png b/packages/frontend/assets/musical_note_3d.png deleted file mode 100644 index 0b520311f6..0000000000 Binary files a/packages/frontend/assets/musical_note_3d.png and /dev/null differ diff --git a/packages/frontend/assets/package_3d.png b/packages/frontend/assets/package_3d.png deleted file mode 100644 index 582134fd2f..0000000000 Binary files a/packages/frontend/assets/package_3d.png and /dev/null differ diff --git a/packages/frontend/assets/prohibited_3d.png b/packages/frontend/assets/prohibited_3d.png deleted file mode 100644 index 1f071edd06..0000000000 Binary files a/packages/frontend/assets/prohibited_3d.png and /dev/null differ diff --git a/packages/frontend/assets/speaker_high_volume_3d.png b/packages/frontend/assets/speaker_high_volume_3d.png deleted file mode 100644 index b25aaa91d6..0000000000 Binary files a/packages/frontend/assets/speaker_high_volume_3d.png and /dev/null differ diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 67a7062285..3cf135bae1 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -9,8 +9,10 @@ "build-storybook-pre": "(tsgo -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js", "build-storybook": "pnpm build-storybook-pre && storybook build --webpack-stats-json storybook-static", "chromatic": "chromatic", - "test": "vitest --run --globals", - "test-and-coverage": "vitest --run --coverage --globals", + "test": "vitest --run --globals --config vitest.config.unit.ts", + "test-and-coverage": "vitest --run --coverage --globals --config vitest.config.unit.ts", + "test:e2e": "playwright test --config playwright.config.ts", + "test:browser": "vitest --run --globals --config vitest.config.browser.ts", "typecheck": "vue-tsc --noEmit", "eslint": "eslint --quiet \"src/**/*.{ts,vue}\"", "lint": "pnpm typecheck && pnpm eslint" @@ -71,6 +73,7 @@ "devDependencies": { "@misskey-dev/emoji-assets": "17.0.3", "@misskey-dev/summaly": "5.5.1", + "@playwright/test": "1.61.1", "@rollup/plugin-json": "6.1.0", "@rollup/pluginutils": "5.4.0", "@storybook/addon-essentials": "8.6.18", @@ -104,6 +107,7 @@ "@types/tinycolor2": "1.4.6", "@typescript-eslint/eslint-plugin": "8.62.0", "@typescript-eslint/parser": "8.62.0", + "@vitest/browser-playwright": "4.1.9", "@vitest/coverage-v8": "4.1.9", "@vue/compiler-core": "3.5.39", "astring": "1.9.0", @@ -120,6 +124,7 @@ "msw-storybook-addon": "2.0.7", "nodemon": "3.1.14", "oxc-walker": "1.0.0", + "playwright": "1.61.1", "prettier": "3.9.1", "react": "19.2.7", "react-dom": "19.2.7", diff --git a/packages/frontend/playwright.config.ts b/packages/frontend/playwright.config.ts new file mode 100644 index 0000000000..2511a929c3 --- /dev/null +++ b/packages/frontend/playwright.config.ts @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './test/e2e', + reporter: 'list', + fullyParallel: false, + workers: 1, + timeout: 60_000, + expect: { + timeout: 10_000, + }, + outputDir: './test/e2e/artifacts', + use: { + locale: 'en-US', + baseURL: 'http://localhost:61812', + headless: true, + screenshot: 'only-on-failure', + trace: 'retain-on-failure', + video: 'off', + }, + projects: [{ + name: 'chromium', + use: { + browserName: 'chromium', + }, + }], +}); diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue index 2fa1135398..1c9de19c25 100644 --- a/packages/frontend/src/components/MkCaptcha.vue +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -21,8 +21,8 @@ SPDX-License-Identifier: AGPL-3.0-only
Type "ai-chan-kawaii" to pass captcha
- - + +
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index fb8b38de6d..c5cb0f3b34 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -31,8 +31,8 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }} - {{ cancelText ?? i18n.ts.cancel }} + {{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }} + {{ cancelText ?? i18n.ts.cancel }}
{{ action.text }} diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index 1def3de97a..f4dd9f526d 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only