From 9472e7267870d62e0db448dc036b30b7985dad8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 8 Jul 2026 20:58:02 +0900 Subject: [PATCH] =?UTF-8?q?enhance(frontend/test):=20frontend=20e2e?= =?UTF-8?q?=E3=82=92Playwright=E3=81=AB=E7=A7=BB=E8=A1=8C=20(#17682)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance(frontend/test): frontend e2eをVitest Browser Modeに移行 * fix * fix unit tests * fix * fix * update playwright * fix * fix * fix * fix * fix * fix * fix * fix * perf * Revert "perf" This reverts commit e60e965c3cc1286c659c0bbf8f2e64fb2ff9d7ff. * fix --- ...tainer.yml => playwright-devcontainer.yml} | 0 .devcontainer/init.sh | 2 +- .github/labeler.yml | 2 +- .github/workflows/check-spdx-license-id.yml | 1 - .github/workflows/test-frontend.yml | 40 +- .gitignore | 7 +- cypress.config.ts | 7 - cypress/e2e/basic.cy.ts | 265 ------ cypress/e2e/router.cy.ts | 35 - cypress/e2e/widgets.cy.ts | 76 -- cypress/fixtures/example.json | 5 - cypress/support/commands.ts | 67 -- cypress/support/e2e.ts | 34 - cypress/support/index.ts | 19 - cypress/tsconfig.json | 8 - package.json | 7 +- packages/frontend/package.json | 9 +- packages/frontend/playwright.config.ts | 32 + .../frontend/src/components/MkCaptcha.vue | 4 +- packages/frontend/src/components/MkDialog.vue | 4 +- packages/frontend/src/components/MkFolder.vue | 2 +- packages/frontend/src/components/MkModal.vue | 2 +- .../frontend/src/components/MkModalWindow.vue | 2 +- .../frontend/src/components/MkPostForm.vue | 4 +- .../src/components/MkServerSetupWizard.vue | 4 +- .../src/components/MkSignin.input.vue | 6 +- .../src/components/MkSignin.password.vue | 6 +- .../src/components/MkSignupDialog.form.vue | 12 +- .../src/components/MkSignupDialog.rules.vue | 4 +- .../src/components/MkSwitch.button.vue | 2 +- .../components/MkUserSetupDialog.Profile.vue | 4 +- .../src/components/MkUserSetupDialog.vue | 26 +- .../src/components/MkVisitorDashboard.vue | 4 +- .../frontend/src/components/MkWidgets.vue | 8 +- .../frontend/src/pages/signup-complete.vue | 2 +- packages/frontend/src/pages/welcome.setup.vue | 12 +- packages/frontend/src/ui/_common_/navbar.vue | 2 +- packages/frontend/src/ui/_common_/widgets.vue | 2 +- .../frontend/src/widgets/WidgetActivity.vue | 2 +- .../frontend/src/widgets/WidgetAichan.vue | 2 +- .../frontend/src/widgets/WidgetAiscript.vue | 2 +- .../frontend/src/widgets/WidgetButton.vue | 2 +- .../frontend/src/widgets/WidgetCalendar.vue | 2 +- packages/frontend/src/widgets/WidgetClock.vue | 2 +- .../src/widgets/WidgetDigitalClock.vue | 2 +- .../frontend/src/widgets/WidgetFederation.vue | 2 +- .../frontend/src/widgets/WidgetJobQueue.vue | 2 +- packages/frontend/src/widgets/WidgetMemo.vue | 2 +- .../src/widgets/WidgetNotifications.vue | 2 +- .../src/widgets/WidgetOnlineUsers.vue | 2 +- .../frontend/src/widgets/WidgetPhotos.vue | 2 +- .../frontend/src/widgets/WidgetPostForm.vue | 2 +- packages/frontend/src/widgets/WidgetRss.vue | 2 +- .../frontend/src/widgets/WidgetSlideshow.vue | 2 +- .../frontend/src/widgets/WidgetTimeline.vue | 2 +- .../frontend/src/widgets/WidgetTrends.vue | 2 +- .../src/widgets/server-metric/index.vue | 2 +- packages/frontend/test/e2e/basic.spec.ts | 221 +++++ packages/frontend/test/e2e/fixtures.ts | 44 + .../frontend/test/e2e/ignorable-errors.ts | 14 + packages/frontend/test/e2e/router.spec.ts | 41 + packages/frontend/test/e2e/utils.ts | 94 +++ .../frontend/test/{init.ts => setup.unit.ts} | 0 .../test/{ => unit}/aiscript/api.test.ts | 0 .../test/{ => unit}/aiscript/common.test.ts | 0 .../test/{ => unit}/aiscript/ui.test.ts | 0 .../test/{ => unit}/autocomplete.test.ts | 0 .../frontend/test/{ => unit}/emoji.test.ts | 2 +- .../frontend/test/{ => unit}/home.test.ts | 1 - .../frontend/test/{ => unit}/i18n.test.ts | 2 +- .../test/{ => unit}/intl-string.test.ts | 0 .../test/{ => unit}/is-birthday.test.ts | 0 .../{ => unit}/lib/nirax/fallbacks.test.ts | 0 .../test/{ => unit}/lib/nirax/fixture.ts | 0 .../{ => unit}/lib/nirax/navigation.test.ts | 0 .../test/{ => unit}/lib/nirax/resolve.test.ts | 0 .../frontend/test/{ => unit}/note.test.ts | 1 - .../frontend/test/{ => unit}/scroll.test.ts | 0 .../frontend/test/{ => unit}/theme.test.ts | 1 - .../test/{ => unit}/url-preview.test.ts | 1 - packages/frontend/vite.config.ts | 17 - packages/frontend/vitest.config.unit.ts | 22 + pnpm-lock.yaml | 759 ++++-------------- 83 files changed, 737 insertions(+), 1249 deletions(-) rename .config/{cypress-devcontainer.yml => playwright-devcontainer.yml} (100%) delete mode 100644 cypress.config.ts delete mode 100644 cypress/e2e/basic.cy.ts delete mode 100644 cypress/e2e/router.cy.ts delete mode 100644 cypress/e2e/widgets.cy.ts delete mode 100644 cypress/fixtures/example.json delete mode 100644 cypress/support/commands.ts delete mode 100644 cypress/support/e2e.ts delete mode 100644 cypress/support/index.ts delete mode 100644 cypress/tsconfig.json create mode 100644 packages/frontend/playwright.config.ts create mode 100644 packages/frontend/test/e2e/basic.spec.ts create mode 100644 packages/frontend/test/e2e/fixtures.ts create mode 100644 packages/frontend/test/e2e/ignorable-errors.ts create mode 100644 packages/frontend/test/e2e/router.spec.ts create mode 100644 packages/frontend/test/e2e/utils.ts rename packages/frontend/test/{init.ts => setup.unit.ts} (100%) rename packages/frontend/test/{ => unit}/aiscript/api.test.ts (100%) rename packages/frontend/test/{ => unit}/aiscript/common.test.ts (100%) rename packages/frontend/test/{ => unit}/aiscript/ui.test.ts (100%) rename packages/frontend/test/{ => unit}/autocomplete.test.ts (100%) rename packages/frontend/test/{ => unit}/emoji.test.ts (96%) rename packages/frontend/test/{ => unit}/home.test.ts (99%) rename packages/frontend/test/{ => unit}/i18n.test.ts (93%) rename packages/frontend/test/{ => unit}/intl-string.test.ts (100%) rename packages/frontend/test/{ => unit}/is-birthday.test.ts (100%) rename packages/frontend/test/{ => unit}/lib/nirax/fallbacks.test.ts (100%) rename packages/frontend/test/{ => unit}/lib/nirax/fixture.ts (100%) rename packages/frontend/test/{ => unit}/lib/nirax/navigation.test.ts (100%) rename packages/frontend/test/{ => unit}/lib/nirax/resolve.test.ts (100%) rename packages/frontend/test/{ => unit}/note.test.ts (99%) rename packages/frontend/test/{ => unit}/scroll.test.ts (100%) rename packages/frontend/test/{ => unit}/theme.test.ts (99%) rename packages/frontend/test/{ => unit}/url-preview.test.ts (99%) create mode 100644 packages/frontend/vitest.config.unit.ts 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..b03f1ee637 100644 --- a/.gitignore +++ b/.gitignore @@ -23,9 +23,8 @@ packages/sw/.yarn/cache # pnpm .pnpm-store -# Cypress -cypress/screenshots -cypress/videos +# Vitest Browser Mode +packages/frontend/test/e2e/screenshots # 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/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/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