mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-26 15:24:39 +02:00
enhance(frontend/test): frontend e2eをVitest Browser Modeに移行
This commit is contained in:
@@ -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
|
||||
|
||||
2
.github/labeler.yml
vendored
2
.github/labeler.yml
vendored
@@ -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:
|
||||
|
||||
1
.github/workflows/check-spdx-license-id.yml
vendored
1
.github/workflows/check-spdx-license-id.yml
vendored
@@ -44,7 +44,6 @@ jobs:
|
||||
}
|
||||
|
||||
directories=(
|
||||
"cypress/e2e"
|
||||
"packages/backend/migration"
|
||||
"packages/backend/src"
|
||||
"packages/backend/test"
|
||||
|
||||
39
.github/workflows/test-frontend.yml
vendored
39
.github/workflows/test-frontend.yml
vendored
@@ -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,13 @@ 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
|
||||
run: pnpm --filter frontend exec playwright install --with-deps chromium
|
||||
- name: Vitest Browser E2E run
|
||||
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: vitest-browser-screenshots
|
||||
path: packages/frontend/test/e2e/screenshots
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
baseUrl: 'http://localhost:61812',
|
||||
},
|
||||
})
|
||||
@@ -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: 投稿フォームのハッシュタグ保持フィールドのテスト
|
||||
@@ -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')
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
*/
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
login(username: string, password: string): Chainable<void>;
|
||||
|
||||
registerUser(
|
||||
username: string,
|
||||
password: string,
|
||||
isAdmin?: boolean
|
||||
): Chainable<void>;
|
||||
|
||||
resetState(): Chainable<void>;
|
||||
|
||||
visitHome(): Chainable<void>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom"],
|
||||
"target": "esnext",
|
||||
"types": ["cypress", "node"]
|
||||
},
|
||||
"include": ["./**/*.ts"]
|
||||
}
|
||||
@@ -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/cypress-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",
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
"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": "vitest --run --globals --config vitest.config.e2e.ts",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"eslint": "eslint --quiet \"src/**/*.{ts,vue}\"",
|
||||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
@@ -102,6 +103,8 @@
|
||||
"@types/textarea-caret": "3.0.4",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@vitest/browser": "4.1.9",
|
||||
"@vitest/browser-playwright": "4.1.9",
|
||||
"@typescript-eslint/eslint-plugin": "8.62.0",
|
||||
"@typescript-eslint/parser": "8.62.0",
|
||||
"@vitest/coverage-v8": "4.1.9",
|
||||
@@ -120,6 +123,7 @@
|
||||
"msw-storybook-addon": "2.0.7",
|
||||
"nodemon": "3.1.14",
|
||||
"oxc-walker": "1.0.0",
|
||||
"playwright": "1.56.0",
|
||||
"prettier": "3.9.1",
|
||||
"react": "19.2.7",
|
||||
"react-dom": "19.2.7",
|
||||
|
||||
61
packages/frontend/test/e2e/basic.browser.test.ts
Normal file
61
packages/frontend/test/e2e/basic.browser.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { commands } from 'vitest/browser';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
describe('Before setup instance', () => {
|
||||
it('successfully loads', async () => {
|
||||
await commands.runScenario('before-setup-loads');
|
||||
});
|
||||
|
||||
it('setup instance', async () => {
|
||||
await commands.runScenario('setup-instance');
|
||||
});
|
||||
});
|
||||
|
||||
describe('After setup instance', () => {
|
||||
it('successfully loads', async () => {
|
||||
await commands.runScenario('after-setup-loads');
|
||||
});
|
||||
|
||||
it('signup', async () => {
|
||||
await commands.runScenario('signup');
|
||||
});
|
||||
|
||||
it('signup with duplicated username', async () => {
|
||||
await commands.runScenario('signup-duplicated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('After user signup', () => {
|
||||
it('signin', async () => {
|
||||
await commands.runScenario('signin');
|
||||
});
|
||||
|
||||
it('suspend', async () => {
|
||||
await commands.runScenario('suspend');
|
||||
});
|
||||
});
|
||||
|
||||
describe('After user signed in', () => {
|
||||
it('successfully loads', async () => {
|
||||
await commands.runScenario('after-signed-in-loads');
|
||||
});
|
||||
|
||||
it('account setup wizard', async () => {
|
||||
await commands.runScenario('account-setup-wizard');
|
||||
});
|
||||
});
|
||||
|
||||
describe('After user setup', () => {
|
||||
it('note', async () => {
|
||||
await commands.runScenario('note');
|
||||
});
|
||||
|
||||
it('open note form with hotkey', async () => {
|
||||
await commands.runScenario('open-note-form-with-hotkey');
|
||||
});
|
||||
});
|
||||
268
packages/frontend/test/e2e/browser-commands.ts
Normal file
268
packages/frontend/test/e2e/browser-commands.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { APIRequestContext, Page } from 'playwright';
|
||||
import type { BrowserCommand } from 'vitest/node';
|
||||
import '@vitest/browser-playwright';
|
||||
|
||||
const BASE_URL = 'http://localhost:61812';
|
||||
const ADMIN_SETUP_PASSWORD = 'example_password_please_change_this_or_you_will_get_hacked';
|
||||
|
||||
interface RegisteredUser {
|
||||
id: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
async function assertOk(status: number, body: string, route: string): Promise<void> {
|
||||
if (status >= 200 && status < 300) return;
|
||||
throw new Error(`${route} failed: status=${status} body=${body}`);
|
||||
}
|
||||
|
||||
async function resetState(request: APIRequestContext): Promise<void> {
|
||||
const response = await request.post(`${BASE_URL}/api/reset-db`);
|
||||
await assertOk(response.status(), await response.text(), '/api/reset-db');
|
||||
}
|
||||
|
||||
async function registerUser(
|
||||
request: APIRequestContext,
|
||||
username: string,
|
||||
password: string,
|
||||
isAdmin = false,
|
||||
): Promise<RegisteredUser> {
|
||||
const route = isAdmin ? '/api/admin/accounts/create' : '/api/signup';
|
||||
const response = await request.post(`${BASE_URL}${route}`, {
|
||||
data: {
|
||||
username,
|
||||
password,
|
||||
...(isAdmin ? { setupPassword: ADMIN_SETUP_PASSWORD } : {}),
|
||||
},
|
||||
});
|
||||
await assertOk(response.status(), await response.text(), route);
|
||||
return await response.json() as RegisteredUser;
|
||||
}
|
||||
|
||||
async function waitApiResponse(page: Page, path: string): Promise<void> {
|
||||
await page.waitForResponse((response) => {
|
||||
return response.url().endsWith(path) && response.request().method() === 'POST';
|
||||
}, { timeout: 30_000 });
|
||||
}
|
||||
|
||||
async function visitHome(page: Page): Promise<void> {
|
||||
await page.goto(`${BASE_URL}/`);
|
||||
await page.locator('button').first().waitFor({ state: 'visible', timeout: 30_000 });
|
||||
}
|
||||
|
||||
async function signInFromTopPage(page: Page, username: string, password: string): Promise<void> {
|
||||
await page.getByTestId('signin').click();
|
||||
await page.getByTestId('signin-page-input').waitFor({ state: 'visible', timeout: 10_000 });
|
||||
await page.locator('[data-cy-signin-username] input').fill(username);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.getByTestId('signin-page-password').waitFor({ state: 'visible', timeout: 10_000 });
|
||||
await page.locator('[data-cy-signin-password] input').fill(password);
|
||||
const signinResponse = waitApiResponse(page, '/api/signin-flow');
|
||||
await page.keyboard.press('Enter');
|
||||
await signinResponse;
|
||||
}
|
||||
|
||||
async function dismissAccountSetupWizard(page: Page): Promise<void> {
|
||||
await page.locator('[data-cy-user-setup] [data-cy-modal-window-close]').click({ timeout: 30_000 });
|
||||
await page.getByTestId('modal-dialog-ok').click();
|
||||
}
|
||||
|
||||
async function prepareAfterSetup(request: APIRequestContext): Promise<void> {
|
||||
await resetState(request);
|
||||
await registerUser(request, 'admin', 'pass', true);
|
||||
}
|
||||
|
||||
async function prepareAfterSignup(page: Page, request: APIRequestContext): Promise<RegisteredUser> {
|
||||
await prepareAfterSetup(request);
|
||||
return await registerUser(request, 'alice', 'alice1234');
|
||||
}
|
||||
|
||||
async function prepareAfterSignedIn(page: Page, request: APIRequestContext): Promise<void> {
|
||||
await prepareAfterSignup(page, request);
|
||||
await visitHome(page);
|
||||
await signInFromTopPage(page, 'alice', 'alice1234');
|
||||
}
|
||||
|
||||
async function prepareAfterUserSetup(page: Page, request: APIRequestContext): Promise<void> {
|
||||
await prepareAfterSignedIn(page, request);
|
||||
await dismissAccountSetupWizard(page);
|
||||
}
|
||||
|
||||
async function runBeforeSetupLoads(page: Page): Promise<void> {
|
||||
await visitHome(page);
|
||||
}
|
||||
|
||||
async function runSetupInstance(page: Page): Promise<void> {
|
||||
await visitHome(page);
|
||||
|
||||
await page.locator('[data-cy-admin-initial-password] input').fill(ADMIN_SETUP_PASSWORD);
|
||||
await page.locator('[data-cy-admin-username] input').fill('admin');
|
||||
await page.locator('[data-cy-admin-password] input').fill('admin1234');
|
||||
const signupResponse = waitApiResponse(page, '/api/admin/accounts/create');
|
||||
await page.getByTestId('admin-ok').click();
|
||||
await signupResponse;
|
||||
|
||||
await page.getByTestId('next').click();
|
||||
await page.locator('[data-cy-server-name] input').fill('Testskey');
|
||||
const updateMetaResponse = waitApiResponse(page, '/api/admin/update-meta');
|
||||
await page.getByTestId('server-setup-wizard-apply').click();
|
||||
await updateMetaResponse;
|
||||
}
|
||||
|
||||
async function runAfterSetupLoads(page: Page, request: APIRequestContext): Promise<void> {
|
||||
await prepareAfterSetup(request);
|
||||
await visitHome(page);
|
||||
}
|
||||
|
||||
async function runSignup(page: Page, request: APIRequestContext): Promise<void> {
|
||||
await prepareAfterSetup(request);
|
||||
await visitHome(page);
|
||||
|
||||
await page.getByTestId('signup').click();
|
||||
await page.getByTestId('signup-rules-continue').waitFor({ state: 'visible' });
|
||||
if (!await page.getByTestId('signup-rules-continue').isDisabled()) {
|
||||
throw new Error('signup-rules-continue should be disabled before agreement');
|
||||
}
|
||||
await page.locator('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
|
||||
await page.getByTestId('modal-dialog-ok').click();
|
||||
await page.getByTestId('signup-rules-continue').click();
|
||||
|
||||
await page.locator('[data-cy-signup-username] input').fill('alice');
|
||||
await page.locator('[data-cy-signup-password] input').fill('alice1234');
|
||||
await page.locator('[data-cy-signup-password-retype] input').fill('alice1234');
|
||||
await page.locator('[data-cy-signup-invitation-code] input').fill('test-invitation-code');
|
||||
const signupResponse = waitApiResponse(page, '/api/signup');
|
||||
await page.getByTestId('signup-submit').click();
|
||||
await signupResponse;
|
||||
}
|
||||
|
||||
async function runSignupDuplicated(page: Page, request: APIRequestContext): Promise<void> {
|
||||
await prepareAfterSetup(request);
|
||||
await registerUser(request, 'alice', 'alice1234');
|
||||
await visitHome(page);
|
||||
|
||||
await page.getByTestId('signup').click();
|
||||
await page.locator('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
|
||||
await page.getByTestId('modal-dialog-ok').click();
|
||||
await page.getByTestId('signup-rules-continue').click();
|
||||
await page.locator('[data-cy-signup-username] input').fill('alice');
|
||||
await page.locator('[data-cy-signup-password] input').fill('alice1234');
|
||||
await page.locator('[data-cy-signup-password-retype] input').fill('alice1234');
|
||||
if (!await page.getByTestId('signup-submit').isDisabled()) {
|
||||
throw new Error('signup-submit should stay disabled for duplicated username');
|
||||
}
|
||||
}
|
||||
|
||||
async function runSignIn(page: Page, request: APIRequestContext): Promise<void> {
|
||||
await prepareAfterSignup(page, request);
|
||||
await visitHome(page);
|
||||
await signInFromTopPage(page, 'alice', 'alice1234');
|
||||
}
|
||||
|
||||
async function runSuspend(page: Page, request: APIRequestContext): Promise<void> {
|
||||
await resetState(request);
|
||||
const admin = await registerUser(request, 'admin', 'pass', true);
|
||||
const alice = await registerUser(request, 'alice', 'alice1234');
|
||||
const suspendResponse = await request.post(`${BASE_URL}/api/admin/suspend-user`, {
|
||||
data: {
|
||||
i: admin.token,
|
||||
userId: alice.id,
|
||||
},
|
||||
});
|
||||
await assertOk(suspendResponse.status(), await suspendResponse.text(), '/api/admin/suspend-user');
|
||||
|
||||
await visitHome(page);
|
||||
await page.getByTestId('signin').click();
|
||||
await page.getByTestId('signin-page-input').waitFor({ state: 'visible', timeout: 10_000 });
|
||||
await page.locator('[data-cy-signin-username] input').fill('alice');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.getByText(/アカウントが凍結されています|This account has been suspended due to/).waitFor({ timeout: 10_000 });
|
||||
}
|
||||
|
||||
async function runAfterSignedInLoads(page: Page, request: APIRequestContext): Promise<void> {
|
||||
await prepareAfterSignedIn(page, request);
|
||||
await page.getByTestId('user-setup-continue').waitFor({ state: 'visible', timeout: 30_000 });
|
||||
}
|
||||
|
||||
async function runAccountSetupWizard(page: Page, request: APIRequestContext): Promise<void> {
|
||||
await prepareAfterSignedIn(page, request);
|
||||
await page.getByTestId('user-setup-continue').click({ timeout: 30_000 });
|
||||
await page.locator('[data-cy-user-setup-user-name] input').fill('ありす');
|
||||
await page.locator('[data-cy-user-setup-user-description] textarea').fill('ほげ');
|
||||
await page.getByTestId('user-setup-continue').click();
|
||||
await page.getByTestId('user-setup-continue').click();
|
||||
await page.getByTestId('user-setup-continue').click();
|
||||
await page.getByTestId('user-setup-continue').click();
|
||||
await page.getByTestId('user-setup-continue').click();
|
||||
}
|
||||
|
||||
async function runCreateNote(page: Page, request: APIRequestContext): Promise<void> {
|
||||
await prepareAfterUserSetup(page, request);
|
||||
await page.getByTestId('open-post-form').waitFor({ state: 'visible' });
|
||||
await page.getByTestId('open-post-form').click();
|
||||
await page.getByTestId('post-form-text').fill('Hello, Misskey!');
|
||||
await page.getByTestId('open-post-form-submit').click();
|
||||
await page.getByText('Hello, Misskey!').waitFor({ timeout: 15_000 });
|
||||
}
|
||||
|
||||
async function runOpenNoteFormWithHotkey(page: Page, request: APIRequestContext): Promise<void> {
|
||||
await prepareAfterUserSetup(page, request);
|
||||
await page.getByTestId('open-post-form').waitFor({ state: 'visible' });
|
||||
await page.evaluate(() => {
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
key: 'n',
|
||||
code: 'KeyL',
|
||||
bubbles: true,
|
||||
}));
|
||||
});
|
||||
await page.getByTestId('post-form-text').waitFor({ state: 'visible' });
|
||||
await page.keyboard.press('Escape');
|
||||
await page.getByTestId('post-form-text').waitFor({ state: 'hidden' });
|
||||
}
|
||||
|
||||
async function runRedirectProfile(page: Page, request: APIRequestContext): Promise<void> {
|
||||
await prepareAfterUserSetup(page, request);
|
||||
await page.goto(`${BASE_URL}/redirect-test`);
|
||||
await page.waitForURL('**/@alice');
|
||||
}
|
||||
|
||||
const scenarioHandlers = {
|
||||
'before-setup-loads': async (page: Page, _request: APIRequestContext) => runBeforeSetupLoads(page),
|
||||
'setup-instance': runSetupInstance,
|
||||
'after-setup-loads': runAfterSetupLoads,
|
||||
signup: runSignup,
|
||||
'signup-duplicated': runSignupDuplicated,
|
||||
signin: runSignIn,
|
||||
suspend: runSuspend,
|
||||
'after-signed-in-loads': runAfterSignedInLoads,
|
||||
'account-setup-wizard': runAccountSetupWizard,
|
||||
note: runCreateNote,
|
||||
'open-note-form-with-hotkey': runOpenNoteFormWithHotkey,
|
||||
'redirect-profile': runRedirectProfile,
|
||||
} satisfies Record<string, (page: Page, request: APIRequestContext) => Promise<void>>;
|
||||
|
||||
export const runScenario: BrowserCommand<[scenario: string]> = async (ctx, scenario) => {
|
||||
if (ctx.provider.name !== 'playwright') {
|
||||
throw new Error(`playwright provider is required, but got ${ctx.provider.name}`);
|
||||
}
|
||||
|
||||
if (!(scenario in scenarioHandlers)) {
|
||||
throw new Error(`unknown e2e scenario: ${scenario}`);
|
||||
}
|
||||
const run = scenarioHandlers[scenario as keyof typeof scenarioHandlers];
|
||||
|
||||
const testPage = await ctx.context.newPage();
|
||||
await testPage.addInitScript(() => {
|
||||
(globalThis as { Cypress?: Record<string, unknown> }).Cypress = { browserMode: true };
|
||||
});
|
||||
|
||||
try {
|
||||
await run(testPage, testPage.request);
|
||||
} finally {
|
||||
await testPage.close();
|
||||
}
|
||||
};
|
||||
12
packages/frontend/test/e2e/commands.d.ts
vendored
Normal file
12
packages/frontend/test/e2e/commands.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
declare module 'vitest/browser' {
|
||||
interface BrowserCommands {
|
||||
runScenario: (scenario: string) => Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
15
packages/frontend/test/e2e/router.browser.test.ts
Normal file
15
packages/frontend/test/e2e/router.browser.test.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { commands } from 'vitest/browser';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
describe('Router transition', () => {
|
||||
describe('Redirect', () => {
|
||||
it('redirect to user profile', async () => {
|
||||
await commands.runScenario('redirect-profile');
|
||||
});
|
||||
});
|
||||
});
|
||||
26
packages/frontend/test/setup.e2e.ts
Normal file
26
packages/frontend/test/setup.e2e.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
function isIgnorableErrorMessage(message: string): boolean {
|
||||
return [
|
||||
'The source image cannot be decoded',
|
||||
'ResizeObserver loop limit exceeded',
|
||||
'ResizeObserver loop completed with undelivered notifications',
|
||||
].some((text) => message.includes(text));
|
||||
}
|
||||
|
||||
window.addEventListener('error', (event) => {
|
||||
if (isIgnorableErrorMessage(event.message)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
const reason = event.reason;
|
||||
const message = reason instanceof Error ? reason.message : String(reason);
|
||||
if (isIgnorableErrorMessage(message)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
/// <reference types="vitest/config" />
|
||||
import path from 'path';
|
||||
import pluginVue from '@vitejs/plugin-vue';
|
||||
import pluginGlsl from 'vite-plugin-glsl';
|
||||
@@ -261,22 +260,6 @@ export function getConfig(): UserConfig {
|
||||
worker: {
|
||||
format: 'es',
|
||||
},
|
||||
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
setupFiles: ['./test/init.ts'],
|
||||
deps: {
|
||||
optimizer: {
|
||||
web: {
|
||||
include: [
|
||||
// XXX: misskey-dev/browser-image-resizer has no "type": "module"
|
||||
'browser-image-resizer',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
includeSource: ['src/**/*.ts'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
28
packages/frontend/vitest.config.e2e.ts
Normal file
28
packages/frontend/vitest.config.e2e.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineConfig, mergeConfig } from 'vitest/config';
|
||||
import { playwright } from '@vitest/browser-playwright';
|
||||
import { runScenario } from './test/e2e/browser-commands.js';
|
||||
import { getConfig } from './vite.config.js';
|
||||
|
||||
export default mergeConfig(getConfig(), defineConfig({
|
||||
test: {
|
||||
include: ['./test/e2e/**/*.test.ts'],
|
||||
exclude: [],
|
||||
setupFiles: ['./test/setup.e2e.ts'],
|
||||
fileParallelism: false,
|
||||
maxWorkers: 1,
|
||||
testTimeout: 60000,
|
||||
browser: {
|
||||
enabled: true,
|
||||
provider: playwright(),
|
||||
headless: true,
|
||||
screenshotFailures: true,
|
||||
screenshotDirectory: './test/e2e/screenshots',
|
||||
commands: {
|
||||
runScenario,
|
||||
},
|
||||
instances: [{
|
||||
browser: 'chromium',
|
||||
}],
|
||||
},
|
||||
},
|
||||
}));
|
||||
22
packages/frontend/vitest.config.unit.ts
Normal file
22
packages/frontend/vitest.config.unit.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { defineConfig, mergeConfig } from 'vitest/config';
|
||||
import { getConfig } from './vite.config.js';
|
||||
|
||||
export default mergeConfig(getConfig(), defineConfig({
|
||||
test: {
|
||||
include: ['./test/unit/**/*.test.ts'],
|
||||
environment: 'happy-dom',
|
||||
setupFiles: ['./test/init.ts'],
|
||||
deps: {
|
||||
optimizer: {
|
||||
web: {
|
||||
include: [
|
||||
// XXX: misskey-dev/browser-image-resizer has no "type": "module"
|
||||
'browser-image-resizer',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
includeSource: ['src/**/*.ts'],
|
||||
},
|
||||
}));
|
||||
|
||||
750
pnpm-lock.yaml
generated
750
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user