diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml
new file mode 100644
index 00000000..65b195e9
--- /dev/null
+++ b/.github/workflows/code-quality.yml
@@ -0,0 +1,24 @@
+name: Qodana
+on:
+ workflow_dispatch:
+ pull_request:
+ push:
+ branches:
+ - master
+
+jobs:
+ qodana:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ checks: write
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
+ fetch-depth: 0
+ - name: Qodana Scan
+ uses: JetBrains/qodana-action@v2024.1.9
+ env:
+ QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
diff --git a/.github/workflows/health-check.yml b/.github/workflows/health-check.yml
index 16d75747..6bf6bf57 100644
--- a/.github/workflows/health-check.yml
+++ b/.github/workflows/health-check.yml
@@ -16,14 +16,13 @@ jobs:
- uses: pnpm/action-setup@v4
with:
- version: 9
run_install: false
# https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping
- - name: Use Node.js 22.x
+ - name: Use Node.js 24.x
uses: actions/setup-node@v4
with:
- node-version: 22.x
+ node-version: 24.x
cache: pnpm
- name: Install dependencies
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
deleted file mode 100644
index 766de744..00000000
--- a/.github/workflows/pull-request.yml
+++ /dev/null
@@ -1,102 +0,0 @@
-name: Pull request
-
-on:
- pull_request:
- paths-ignore:
- - '**.md'
-
-env:
- CI: true
-
-jobs:
- lint:
- name: Lint
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - uses: pnpm/action-setup@v4
- with:
- version: 9
- run_install: false
-
- - name: Use Node.js 22.x
- uses: actions/setup-node@v4
- with:
- node-version: 22.x
- cache: pnpm
-
- - name: Install dependencies
- run: pnpm install --no-frozen-lockfile
-
- - name: TypeScript check
- run: pnpm lint
-
- - name: Eslint check
- run: pnpm lint
-
- unit_test:
- name: Unit test
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - uses: pnpm/action-setup@v4
- with:
- version: 9
- run_install: false
-
- - name: Use Node.js 22.x
- uses: actions/setup-node@v4
- with:
- node-version: 22.x
- cache: pnpm
-
- - name: Install dependencies
- run: pnpm install --no-frozen-lockfile
-
- - name: Unit test
- run: pnpm test:unit
-
- - name: Update coverage report
- uses: codecov/codecov-action@v4
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
-
- e2e_tests:
- name: E2E test
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - uses: pnpm/action-setup@v4
- with:
- version: 9
- run_install: false
-
- - name: Use Node.js 22.x
- uses: actions/setup-node@v4
- with:
- node-version: 22.x
- cache: pnpm
-
- - name: Install dependencies
- run: pnpm install --no-frozen-lockfile
-
- - name: Get cypress version
- id: cypress-version
- run: echo "version=$(pnpm info cypress version)" >> $GITHUB_OUTPUT
-
- - name: Cache cypress binary
- id: cache-cypress-binary
- uses: actions/cache@v4
- with:
- path: ~/.cache/Cypress
- key: cypress-binary-${{ runner.os }}-${{ steps.cypress-version.outputs.version }}
-
- - name: Install cypress binary
- if: steps.cache-cypress-binary.outputs.cache-hit != 'true'
- run: pnpm cypress install
-
- - name: E2E test
- run: pnpm test:e2e:ci
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ea03ded1..559f5090 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -4,6 +4,9 @@ on:
push:
paths-ignore:
- '**.md'
+ pull_request:
+ paths-ignore:
+ - '**.md'
env:
CI: true
@@ -17,13 +20,12 @@ jobs:
- uses: pnpm/action-setup@v4
with:
- version: 9
run_install: false
- - name: Use Node.js 22.x
+ - name: Use Node.js 24.x
uses: actions/setup-node@v4
with:
- node-version: 22.x
+ node-version: 24.x
cache: pnpm
- name: Install dependencies
@@ -43,13 +45,12 @@ jobs:
- uses: pnpm/action-setup@v4
with:
- version: 9
run_install: false
- - name: Use Node.js 22.x
+ - name: Use Node.js 24.x
uses: actions/setup-node@v4
with:
- node-version: 22.x
+ node-version: 24.x
cache: pnpm
- name: Install dependencies
@@ -63,21 +64,20 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
- e2e_tests:
- name: E2E test
+ cypress_e2e_tests:
+ name: Cypress E2E test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
- version: 9
run_install: false
- - name: Use Node.js 22.x
+ - name: Use Node.js 24.x
uses: actions/setup-node@v4
with:
- node-version: 22.x
+ node-version: 24.x
cache: pnpm
- name: Install dependencies
@@ -99,4 +99,36 @@ jobs:
run: pnpm cypress install
- name: E2E test
- run: pnpm test:e2e:ci
+ run: pnpm test:cypress
+
+ playwright_e2e_tests:
+ name: Playwright E2E test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+ with:
+ run_install: false
+
+ - name: Use Node.js 24.x
+ uses: actions/setup-node@v4
+ with:
+ node-version: 24.x
+ cache: pnpm
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Install playwright binary
+ run: pnpm playwright install --with-deps
+
+ - name: E2E test
+ run: pnpm test:playwright
+
+ - uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 30
diff --git a/.gitignore b/.gitignore
index 38adffa6..7bfad8c6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,11 @@ coverage
/cypress/videos/
/cypress/screenshots/
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
+
# Editor directories and files
.vscode/*
!.vscode/extensions.json
diff --git a/cypress/fixtures/article.json b/cypress/fixtures/article.json
index 82bd64cd..a3d000f3 100644
--- a/cypress/fixtures/article.json
+++ b/cypress/fixtures/article.json
@@ -5,7 +5,7 @@
"body": "# Article body\n\nThis is **Strong** text",
"createdAt": "2020-11-01T14:59:39.404Z",
"updatedAt": "2020-11-01T14:59:39.404Z",
- "tagList": [],
+ "tagList": ["foo", "bar"],
"description": "this is descripion",
"author": {
"username": "plumrx",
diff --git a/eslint.config.js b/eslint.config.js
index 44d8826c..757ab49e 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -26,4 +26,12 @@ export default defineConfig({
rules: {
'ts/method-signature-style': 'off',
},
+}, {
+ files: [
+ '*.config.ts',
+ 'playwright/**/*',
+ ],
+ rules: {
+ 'node/prefer-global/process': 'off',
+ },
})
diff --git a/index.html b/index.html
index 08e2c793..6142a68f 100644
--- a/index.html
+++ b/index.html
@@ -6,10 +6,10 @@
-
-
+
+
-
+
diff --git a/package.json b/package.json
index 479b8689..9be6ab5d 100644
--- a/package.json
+++ b/package.json
@@ -5,16 +5,19 @@
"type": "module",
"scripts": {
"prepare": "simple-git-hooks",
- "dev": "vite",
+ "dev": "vite --port 4173",
"build": "vite build",
"serve": "vite preview --port 4173",
"type-check": "vue-tsc --noEmit",
"lint": "eslint --fix .",
- "test": "npm run test:unit && npm run test:e2e:ci",
- "test:e2e": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress open --e2e -c baseUrl=http://localhost:4173\"",
- "test:e2e:ci": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress run --e2e -c baseUrl=http://localhost:4173\"",
- "test:e2e:local": "cypress open --e2e -c baseUrl=http://localhost:5173",
- "test:e2e:prod": "cypress run --e2e -c baseUrl=https://vue3-realworld-example-app-mutoe.vercel.app",
+ "test": "npm run test:unit && npm run test:playwright",
+ "test:cypress": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress run --e2e",
+ "test:cypress:ui": "cypress open --e2e",
+ "test:cyprsss:prod": "cypress run --e2e -c baseUrl=https://vue3-realworld-example-app-mutoe.vercel.app",
+ "test:playwright": "npm run build && cross-env CI=true playwright test",
+ "test:playwright:prod": "cross-env E2E_BASE_URL='https://vue3-realworld-example-app-mutoe.vercel.app' playwright test",
+ "test:playwright:ui": "playwright test --ui",
+ "test:playwright:ui:debug": "playwright test --ui --headed --debug",
"test:unit": "vitest run",
"generate:api": "curl -sL https://raw.githubusercontent.com/gothinkster/realworld/refs/heads/main/api/openapi.yml -o ./src/services/openapi.yml && sta generate -p ./src/services/openapi.yml -o ./src/services -n api.ts"
},
@@ -28,16 +31,22 @@
"devDependencies": {
"@mutoe/eslint-config": "^4.11.0-2",
"@pinia/testing": "^1.0.2",
+ "@playwright/test": "^1.55.1",
"@testing-library/cypress": "^10.1.0",
"@testing-library/user-event": "^14.6.1",
"@testing-library/vue": "^8.1.0",
+ "@types/html": "^1.0.4",
+ "@types/node": "^24.5.2",
"@vitejs/plugin-vue": "^6.0.1",
"@vitest/coverage-v8": "^3.2.4",
"concurrently": "^9.2.1",
+ "cross-env": "^7.0.3",
"cypress": "^13.13.2",
"eslint": "^9.36.0",
"eslint-plugin-cypress": "^5.1.1",
+ "eslint-plugin-vuejs-accessibility": "^2.4.1",
"happy-dom": "^18.0.1",
+ "html": "^1.0.0",
"lint-staged": "^16.2.0",
"msw": "^2.11.3",
"rollup-plugin-analyzer": "^4.0.0",
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 00000000..738b6436
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,77 @@
+import { defineConfig, devices } from '@playwright/test'
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// import dotenv from 'dotenv';
+// dotenv.config({ path: path.resolve(__dirname, '.env') });
+
+const isCI = process.env.CI
+const baseURL = process.env.E2E_BASE_URL || 'http://localhost:4173'
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './playwright',
+ /* Run tests in files in parallel */
+ fullyParallel: false,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!isCI,
+ /* Retry on CI only */
+ retries: isCI ? 1 : 0,
+ /* Opt out of parallel tests on CI. */
+ workers: isCI ? 1 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: [
+ ['html', { open: 'never' }],
+ isCI ? ['github'] : ['list'],
+ ],
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+
+ navigationTimeout: isCI ? 10_000 : 4000,
+ actionTimeout: isCI ? 10_000 : 4000,
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ screenshot: 'only-on-failure',
+ trace: isCI ? 'on-first-retry' : 'retain-on-failure',
+ video: isCI ? 'on-first-retry' : 'retain-on-failure',
+ },
+
+ /* Configure projects for major browsers */
+ projects: isCI
+ ? [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ },
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ },
+ ]
+ : [
+ {
+ name: 'chromium',
+ use: {
+ ...devices['Desktop Chrome'],
+ },
+ },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: isCI ? 'pnpm serve' : 'npm run dev',
+ url: baseURL,
+ reuseExistingServer: !isCI,
+ ignoreHTTPSErrors: true,
+ },
+})
diff --git a/playwright/constant.ts b/playwright/constant.ts
new file mode 100644
index 00000000..0f2a523f
--- /dev/null
+++ b/playwright/constant.ts
@@ -0,0 +1,8 @@
+export enum Route {
+ Home = '/#/',
+ Login = '/#/login',
+ Register = '/#/register',
+ Settings = '/#/settings',
+ ArticleCreate = '/#/article/create',
+ ArticleDetail = '/#/article/article-title',
+}
diff --git a/playwright/extends.ts b/playwright/extends.ts
new file mode 100644
index 00000000..c1113b2b
--- /dev/null
+++ b/playwright/extends.ts
@@ -0,0 +1,22 @@
+import { test as base } from '@playwright/test'
+import { ConduitPageObject } from 'page-objects/conduit.page-object'
+
+export const test = base.extend<{
+ conduit: ConduitPageObject
+}>({
+ conduit: async ({ page }, use) => {
+ const buyscoutPageObject = new ConduitPageObject(page)
+ await use(buyscoutPageObject)
+ },
+})
+
+test.afterEach(async ({ page }, testInfo) => {
+ if (!process.env.CI && testInfo.status !== testInfo.expectedStatus) {
+ // eslint-disable-next-line ts/restrict-template-expressions
+ process.stderr.write(`❌ ❌ PLAYWRIGHT TEST FAILURE ❌ ❌\n${testInfo.error?.stack || testInfo.error}\n`)
+ testInfo.setTimeout(0)
+ await page.pause()
+ }
+})
+
+export const expect = test.expect
diff --git a/playwright/page-objects/article-detail.page-object.ts b/playwright/page-objects/article-detail.page-object.ts
new file mode 100644
index 00000000..162228a0
--- /dev/null
+++ b/playwright/page-objects/article-detail.page-object.ts
@@ -0,0 +1,33 @@
+import type { Page } from '@playwright/test'
+import { ConduitPageObject } from './conduit.page-object'
+
+export class ArticleDetailPageObject extends ConduitPageObject {
+ constructor(public page: Page) {
+ super(page)
+ }
+
+ positionMap = {
+ 'banner': 0,
+ 'article footer': 1,
+ } as const
+
+ private async clickOperationButton(position: keyof typeof this.positionMap = 'banner', buttonName: string) {
+ await this.page.getByRole('button', { name: buttonName }).nth(this.positionMap[position]).click()
+ }
+
+ async clickEditArticle(position: keyof typeof this.positionMap = 'banner') {
+ return await this.clickOperationButton(position, 'Edit Article')
+ }
+
+ async clickDeleteArticle(position: keyof typeof this.positionMap = 'banner') {
+ await this.page.getByRole('button', { name: 'Delete article' }).nth(this.positionMap[position]).dispatchEvent('click')
+ }
+
+ async clickFollowUser(position: keyof typeof this.positionMap = 'banner') {
+ await this.page.getByRole('button', { name: 'Follow' }).nth(this.positionMap[position]).dispatchEvent('click')
+ }
+
+ async clickFavoriteArticle(position: keyof typeof this.positionMap = 'banner') {
+ await this.page.getByRole('button', { name: 'Favorite article' }).nth(this.positionMap[position]).dispatchEvent('click')
+ }
+}
diff --git a/playwright/page-objects/conduit.page-object.ts b/playwright/page-objects/conduit.page-object.ts
new file mode 100644
index 00000000..b5d665b3
--- /dev/null
+++ b/playwright/page-objects/conduit.page-object.ts
@@ -0,0 +1,86 @@
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import url from 'node:url'
+import type { Page, Response } from '@playwright/test'
+import type { User } from 'src/services/api.ts'
+import { Route } from '../constant'
+import { expect } from '../extends.ts'
+import { boxedStep } from '../utils/test-decorators.ts'
+
+const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
+const fixtureDir = path.join(__dirname, '../../cypress/fixtures')
+
+export class ConduitPageObject {
+ constructor(
+ public readonly page: Page,
+ ) {}
+
+ async intercept(method: 'POST' | 'GET' | 'PATCH' | 'DELETE' | 'PUT', url: string | RegExp, options: {
+ fixture?: string
+ postFixture?: (fixture: any) => void | unknown
+ statusCode?: number
+ body?: unknown
+ timeout?: number
+ } = {}): Promise<() => Promise> {
+ await this.page.route(url, async route => {
+ if (route.request().method() !== method)
+ return await route.continue()
+
+ if (options.postFixture && options.fixture) {
+ const body = await this.getFixture(options.fixture)
+ const returnValue = await options.postFixture(body)
+ options.body = returnValue === undefined ? body : returnValue
+ options.fixture = undefined
+ }
+
+ return await route.fulfill({
+ status: options.statusCode || undefined,
+ json: options.body ?? undefined,
+ path: options.fixture ? path.join(fixtureDir, options.fixture) : undefined,
+ })
+ })
+
+ return () => this.page.waitForResponse(response => {
+ const request = response.request()
+ if (request.method() !== method)
+ return false
+
+ if (typeof url === 'string')
+ return request.url().includes(url)
+
+ return url.test(request.url())
+ }, { timeout: options.timeout ?? 4000 })
+ }
+
+ async getFixture(fixture: string): Promise {
+ const file = path.join(fixtureDir, fixture)
+ return JSON.parse(await fs.readFile(file, 'utf8')) as T
+ }
+
+ async goto(route: Route) {
+ await this.page.goto(route, { waitUntil: 'domcontentloaded' })
+ }
+
+ @boxedStep
+ async login(username = 'plumrx') {
+ const userFixture = await this.getFixture<{ user: User }>('user.json')
+ userFixture.user.username = username
+
+ await this.goto(Route.Login)
+
+ await this.page.getByPlaceholder('Email').fill('foo@example.com')
+ await this.page.getByPlaceholder('Password').fill('12345678')
+
+ const waitForLogin = await this.intercept('POST', /users\/login$/, { statusCode: 200, body: userFixture })
+ await Promise.all([
+ waitForLogin(),
+ this.page.getByRole('button', { name: 'Sign in' }).click(),
+ ])
+
+ await expect(this.page).toHaveURL(Route.Home)
+ }
+
+ async toContainText(text: string) {
+ await expect(this.page.locator('body')).toContainText(text)
+ }
+}
diff --git a/playwright/page-objects/edit-article.page-object.ts b/playwright/page-objects/edit-article.page-object.ts
new file mode 100644
index 00000000..fed9cd4b
--- /dev/null
+++ b/playwright/page-objects/edit-article.page-object.ts
@@ -0,0 +1,44 @@
+import type { Page } from '@playwright/test'
+import { ConduitPageObject } from './conduit.page-object.ts'
+
+export class EditArticlePageObject extends ConduitPageObject {
+ constructor(public page: Page) {
+ super(page)
+ }
+
+ async fillTitle(title: string) {
+ await this.page.getByPlaceholder('Article Title').fill(title)
+ }
+
+ async fillDescription(description: string) {
+ await this.page.getByPlaceholder("What's this article about?").fill(description)
+ }
+
+ async fillContent(content: string) {
+ await this.page.getByPlaceholder('Write your article (in markdown)').fill(content)
+ }
+
+ async fillTags(tags: string | string[]) {
+ if (!Array.isArray(tags))
+ tags = [tags]
+ for (const tag of tags) {
+ await this.page.getByPlaceholder('Enter tags').fill(tag)
+ await this.page.getByPlaceholder('Enter tags').press('Enter')
+ }
+ }
+
+ async fillForm({ title, description, content, tags }: { title?: string, description?: string, content?: string, tags?: string | string[] }) {
+ if (title !== undefined)
+ await this.fillTitle(title)
+ if (description !== undefined)
+ await this.fillDescription(description)
+ if (content !== undefined)
+ await this.fillContent(content)
+ if (tags !== undefined)
+ await this.fillTags(tags)
+ }
+
+ async clickPublishArticle() {
+ await this.page.getByRole('button', { name: 'Publish Article' }).dispatchEvent('click')
+ }
+}
diff --git a/playwright/page-objects/login.page-object.ts b/playwright/page-objects/login.page-object.ts
new file mode 100644
index 00000000..ffd93711
--- /dev/null
+++ b/playwright/page-objects/login.page-object.ts
@@ -0,0 +1,27 @@
+import type { Page } from '@playwright/test'
+import { ConduitPageObject } from './conduit.page-object.ts'
+
+export class LoginPageObject extends ConduitPageObject {
+ constructor(public page: Page) {
+ super(page)
+ }
+
+ async fillEmail(email: string = 'foo@example.com') {
+ await this.page.getByPlaceholder('Email').fill(email)
+ }
+
+ async fillPassword(password = '12345678') {
+ await this.page.getByPlaceholder('Password').fill(password)
+ }
+
+ async fillForm(form: { email?: string, password?: string }) {
+ if (form.email !== undefined)
+ await this.fillEmail(form.email)
+ if (form.password !== undefined)
+ await this.fillPassword(form.password)
+ }
+
+ async clickSignIn() {
+ await this.page.getByRole('button', { name: 'Sign in' }).dispatchEvent('click')
+ }
+}
diff --git a/playwright/page-objects/register.page-object.ts b/playwright/page-objects/register.page-object.ts
new file mode 100644
index 00000000..c6db96a7
--- /dev/null
+++ b/playwright/page-objects/register.page-object.ts
@@ -0,0 +1,33 @@
+import type { Page } from '@playwright/test'
+import { ConduitPageObject } from './conduit.page-object.ts'
+
+export class RegisterPageObject extends ConduitPageObject {
+ constructor(public page: Page) {
+ super(page)
+ }
+
+ async fillName(name: string = 'foo') {
+ await this.page.getByPlaceholder('Your Name').fill(name)
+ }
+
+ async fillEmail(email: string = 'foo@example.com') {
+ await this.page.getByPlaceholder('Email').fill(email)
+ }
+
+ async fillPassword(password = '12345678') {
+ await this.page.getByPlaceholder('Password').fill(password)
+ }
+
+ async fillForm(form: { name?: string, email?: string, password?: string }) {
+ if (form.name !== undefined)
+ await this.fillName(form.name)
+ if (form.email !== undefined)
+ await this.fillEmail(form.email)
+ if (form.password !== undefined)
+ await this.fillPassword(form.password)
+ }
+
+ async clickSignUp() {
+ await this.page.getByRole('button', { name: 'Sign up' }).dispatchEvent('click')
+ }
+}
diff --git a/playwright/specs/article.spec.ts b/playwright/specs/article.spec.ts
new file mode 100644
index 00000000..79f8d8e2
--- /dev/null
+++ b/playwright/specs/article.spec.ts
@@ -0,0 +1,139 @@
+import { ArticleDetailPageObject } from 'page-objects/article-detail.page-object.ts'
+import { EditArticlePageObject } from 'page-objects/edit-article.page-object.ts'
+import type { Article } from 'src/services/api.ts'
+import { Route } from '../constant.ts'
+import { expect, test } from '../extends'
+import { formatHTML, formatJSON } from '../utils/prettify.ts'
+
+test.beforeEach(async ({ conduit }) => {
+ await conduit.intercept('GET', /articles\?limit/, { fixture: 'articles.json' })
+ await conduit.intercept('GET', /tags/, { fixture: 'tags.json' })
+ await conduit.intercept('GET', /profiles\/.+/, { fixture: 'profile.json' })
+
+ await conduit.login()
+})
+
+test.describe('post article', () => {
+ let editArticlePage!: EditArticlePageObject
+
+ test.beforeEach(({ page }) => {
+ editArticlePage = new EditArticlePageObject(page)
+ })
+
+ test('jump to post detail page when submit create article form', async ({ page, conduit }) => {
+ await conduit.goto(Route.ArticleCreate)
+
+ const articleFixture = await conduit.getFixture<{ article: Article }>('article.json')
+ const waitForPostArticle = await editArticlePage.intercept('POST', /articles$/, { body: articleFixture })
+
+ await editArticlePage.fillForm({
+ title: articleFixture.article.title,
+ description: articleFixture.article.description,
+ content: articleFixture.article.body,
+ tags: articleFixture.article.tagList,
+ })
+
+ await editArticlePage.clickPublishArticle()
+ await waitForPostArticle()
+
+ await conduit.intercept('GET', /articles\/.+/, { fixture: 'article.json' })
+ await page.waitForURL(/article\/article-title/)
+ await conduit.toContainText('Article title')
+ })
+
+ test('should render markdown correctly', async ({ browserName, page, conduit }) => {
+ test.skip(browserName !== 'chromium')
+ const waitForArticleRequest = await conduit.intercept('GET', /articles\/.+/, { fixture: 'article.json' })
+ await Promise.all([
+ waitForArticleRequest(),
+ conduit.goto(Route.ArticleDetail),
+ ])
+ const innerHTML = await page.locator('.article-content').innerHTML()
+ expect(formatHTML(innerHTML)).toMatchSnapshot('markdown-render.html')
+ })
+})
+
+test.describe('delete article', () => {
+ for (const position of ['banner', 'article footer'] as const) {
+ test(`delete article from ${position}`, async ({ page, conduit }) => {
+ const articlePage = new ArticleDetailPageObject(page)
+ const waitForArticle = await articlePage.intercept('GET', /articles\/.+/, { fixture: 'article.json' })
+ await conduit.goto(Route.ArticleDetail)
+ await waitForArticle()
+
+ const waitForDeleteArticle = await conduit.intercept('DELETE', /articles\/.+/)
+
+ const [response] = await Promise.all([
+ waitForDeleteArticle(),
+ articlePage.clickDeleteArticle(position),
+ ])
+
+ expect(response).toBeInstanceOf(Object)
+ await expect(page).toHaveURL(Route.Home)
+ })
+ }
+})
+
+test.describe('favorite article', () => {
+ test.beforeEach(async ({ conduit }) => {
+ await conduit.intercept('GET', /tags/, { fixture: 'tags.json' })
+ })
+
+ test('should jump to login page when click favorite article button given user not logged', async ({ page, conduit }) => {
+ await conduit.goto(Route.Home)
+
+ const waitForFavoriteArticle = await conduit.intercept('POST', /articles\/\S+\/favorite$/, { statusCode: 401 })
+ await Promise.all([
+ waitForFavoriteArticle(),
+ page.getByRole('button', { name: 'Favorite article' }).first().click(),
+ ])
+
+ await expect(page).toHaveURL(Route.Login)
+ })
+
+ test('should call favorite api and highlight favorite button when click favorite button', async ({ page, conduit }) => {
+ await conduit.login()
+ await conduit.goto(Route.Home)
+
+ // like articles
+ const waitForFavoriteArticle = await conduit.intercept('POST', /articles\/\S+\/favorite$/, { fixture: 'article.json' })
+ await Promise.all([
+ waitForFavoriteArticle(),
+ page.getByRole('button', { name: 'Favorite article' }).first().click(),
+ ])
+
+ await expect(page.getByRole('button', { name: 'Favorite article' }).first()).toHaveClass('btn-primary')
+ })
+})
+
+test.describe('tag', () => {
+ test.beforeEach(async ({ conduit }) => {
+ await conduit.intercept('GET', /articles\?tag=butt/, { fixture: 'articles-of-tag.json' })
+ })
+
+ test('should display popular tags in home page', async ({ page, conduit }) => {
+ await conduit.goto(Route.Home)
+
+ const tagItems = await page.getByText('Popular Tags')
+ .locator('..')
+ .locator('.tag-pill')
+ .all()
+ .then(items => Promise.all(items.map(item => item.textContent())))
+ expect(tagItems).toHaveLength(8)
+ expect(formatJSON(tagItems)).toMatchSnapshot('popular-tags-in-home-page.json')
+ })
+
+ test('should show right articles of tag', async ({ page, conduit }) => {
+ const tagName = 'butt'
+ await conduit.goto(Route.Home)
+
+ await conduit.intercept('GET', /articles\?tag/, { fixture: 'articles-of-tag.json' })
+ await page.getByLabel(tagName).click()
+
+ await expect(page).toHaveURL(`/#/tag/${tagName}`)
+ await expect(page.locator('a.tag-pill.tag-default').last())
+ .toHaveClass(/(router-link-active|router-link-exact-active)/)
+
+ await expect(page.getByLabel('tag')).toContainText('butt')
+ })
+})
diff --git a/playwright/specs/article.spec.ts-snapshots/markdown-render-chromium-darwin.html b/playwright/specs/article.spec.ts-snapshots/markdown-render-chromium-darwin.html
new file mode 100644
index 00000000..c4567bb8
--- /dev/null
+++ b/playwright/specs/article.spec.ts-snapshots/markdown-render-chromium-darwin.html
@@ -0,0 +1,8 @@
+
+
Article body
+
This is Strong text
+
+
diff --git a/playwright/specs/article.spec.ts-snapshots/popular-tags-in-home-page-chromium-darwin.json b/playwright/specs/article.spec.ts-snapshots/popular-tags-in-home-page-chromium-darwin.json
new file mode 100644
index 00000000..46541c56
--- /dev/null
+++ b/playwright/specs/article.spec.ts-snapshots/popular-tags-in-home-page-chromium-darwin.json
@@ -0,0 +1,10 @@
+[
+ "HuManIty",
+ "Gandhi",
+ "HITLER",
+ "SIDA",
+ "BlackLivesMatter",
+ "test",
+ "dragons",
+ "butt"
+]
diff --git a/playwright/specs/article.spec.ts-snapshots/popular-tags-in-home-page-firefox-darwin.json b/playwright/specs/article.spec.ts-snapshots/popular-tags-in-home-page-firefox-darwin.json
new file mode 100644
index 00000000..46541c56
--- /dev/null
+++ b/playwright/specs/article.spec.ts-snapshots/popular-tags-in-home-page-firefox-darwin.json
@@ -0,0 +1,10 @@
+[
+ "HuManIty",
+ "Gandhi",
+ "HITLER",
+ "SIDA",
+ "BlackLivesMatter",
+ "test",
+ "dragons",
+ "butt"
+]
diff --git a/playwright/specs/article.spec.ts-snapshots/popular-tags-in-home-page-webkit-darwin.json b/playwright/specs/article.spec.ts-snapshots/popular-tags-in-home-page-webkit-darwin.json
new file mode 100644
index 00000000..46541c56
--- /dev/null
+++ b/playwright/specs/article.spec.ts-snapshots/popular-tags-in-home-page-webkit-darwin.json
@@ -0,0 +1,10 @@
+[
+ "HuManIty",
+ "Gandhi",
+ "HITLER",
+ "SIDA",
+ "BlackLivesMatter",
+ "test",
+ "dragons",
+ "butt"
+]
diff --git a/playwright/specs/auth.spec.ts b/playwright/specs/auth.spec.ts
new file mode 100644
index 00000000..239653c6
--- /dev/null
+++ b/playwright/specs/auth.spec.ts
@@ -0,0 +1,124 @@
+import { LoginPageObject } from 'page-objects/login.page-object.ts'
+import { RegisterPageObject } from 'page-objects/register.page-object.ts'
+import { Route } from '../constant.ts'
+import { expect, test } from '../extends'
+
+test.beforeEach(async ({ conduit }) => {
+ await conduit.intercept('GET', /users/, { fixture: 'user.json' })
+ await conduit.intercept('GET', /tags/, { fixture: 'tags.json' })
+ await conduit.intercept('GET', /articles/, { fixture: 'articles.json' })
+})
+
+test.describe('login and logout', () => {
+ let loginPage!: LoginPageObject
+
+ test.beforeEach(({ page }) => {
+ loginPage = new LoginPageObject(page)
+ })
+
+ test('should login success when submit a valid login form', async ({ page, conduit }) => {
+ await conduit.login()
+
+ await expect(page).toHaveURL(Route.Home)
+ })
+
+ test('should logout when click logout button', async ({ page, conduit }) => {
+ await conduit.login()
+ await conduit.goto(Route.Settings)
+
+ await page.getByRole('button', { name: 'logout' }).click()
+ await conduit.toContainText('Sign in')
+ })
+
+ test('should display error when submit an invalid form (password not match)', async ({ conduit }) => {
+ await conduit.goto(Route.Login)
+
+ await loginPage.intercept('POST', /users\/login/, {
+ statusCode: 403,
+ body: { errors: { 'email or password': ['is invalid'] } },
+ })
+ await loginPage.fillForm({ email: 'foo@example.com', password: '12345678' })
+ await loginPage.clickSignIn()
+
+ await loginPage.toContainText('email or password is invalid')
+ })
+
+ test('should display format error without API call when submit an invalid format', async ({ page, conduit }) => {
+ await conduit.goto(Route.Login)
+
+ await loginPage.intercept('POST', /users\/login/)
+ await loginPage.fillForm({ email: 'foo', password: '123456' })
+ await loginPage.clickSignIn()
+
+ expect(await page.$eval('form', form => form.checkValidity())).toBe(false)
+ })
+
+ test('should not allow visiting login page when the user is logged in', async ({ page, conduit }) => {
+ await conduit.login()
+ await conduit.goto(Route.Login)
+
+ await expect(page).toHaveURL(Route.Home)
+ })
+
+ test('should has credential header after login success', async ({ page, conduit }) => {
+ await conduit.login()
+ await conduit.goto(Route.Settings)
+
+ const waitForUpdateSettingsRequest = await conduit.intercept('PUT', /user/)
+
+ await page.getByRole('textbox', { name: 'Username' }).fill('foo')
+ await page.getByRole('button', { name: 'Update Settings' }).dispatchEvent('click')
+
+ const response = await waitForUpdateSettingsRequest()
+ expect(response.request().headers()).toHaveProperty('authorization')
+ })
+})
+
+test.describe('register', () => {
+ let registerPage!: RegisterPageObject
+
+ test.beforeEach(({ page }) => {
+ registerPage = new RegisterPageObject(page)
+ })
+
+ test('should call register API and jump to home page when submit a valid form', async ({ conduit }) => {
+ await conduit.goto(Route.Register)
+
+ const waitForRegisterRequest = await registerPage.intercept('POST', /users$/, { fixture: 'user.json' })
+ await registerPage.fillForm({
+ name: 'foo',
+ email: 'foo@example.com',
+ password: '12345678',
+ })
+ await registerPage.clickSignUp()
+
+ await waitForRegisterRequest()
+ await expect(conduit.page).toHaveURL(Route.Home)
+ })
+
+ test('should display error message when submit the form that username already exist', async ({ conduit }) => {
+ await conduit.goto(Route.Register)
+
+ const waitForRegisterRequest = await registerPage.intercept('POST', /users$/, {
+ statusCode: 422,
+ body: { errors: { email: ['has already been taken'], username: ['has already been taken'] } },
+ })
+ await registerPage.fillForm({
+ name: 'foo',
+ email: 'foo@example.com',
+ password: '12345678',
+ })
+ await registerPage.clickSignUp()
+
+ await waitForRegisterRequest()
+ await registerPage.toContainText('email has already been taken')
+ await registerPage.toContainText('username has already been taken')
+ })
+
+ test('should not allow visiting register page when the user is logged in', async ({ page, conduit }) => {
+ await conduit.login()
+ await conduit.goto(Route.Register)
+
+ await expect(page).toHaveURL(Route.Home)
+ })
+})
diff --git a/playwright/specs/home.spec.ts b/playwright/specs/home.spec.ts
new file mode 100644
index 00000000..8c29b73b
--- /dev/null
+++ b/playwright/specs/home.spec.ts
@@ -0,0 +1,59 @@
+import { Route } from '../constant.ts'
+import { expect, test } from '../extends.ts'
+
+test.beforeEach(async ({ conduit }) => {
+ await conduit.intercept('GET', /articles\?tag=butt/, { fixture: 'articles-of-tag.json' })
+ await conduit.intercept('GET', /articles\?limit/, { fixture: 'articles.json' })
+ await conduit.intercept('GET', /articles\/.+/, { fixture: 'article.json' })
+ await conduit.intercept('GET', /tags/, { fixture: 'tags.json' })
+})
+
+test('should can access home page', async ({ page, conduit }) => {
+ await conduit.goto(Route.Home)
+
+ await expect(page.getByRole('heading', { name: 'conduit' })).toContainText('conduit')
+})
+
+test.describe('navigation bar', () => {
+ test('should highlight Home nav-item top menu bar when page load', async ({ page, conduit }) => {
+ await conduit.goto(Route.Home)
+
+ await expect(page.getByRole('link', { name: 'Home', exact: true })).toHaveClass(/active/)
+ })
+})
+
+test.describe('article previews', () => {
+ test('should highlight Global Feed when home page loaded', async ({ page, conduit }) => {
+ await conduit.goto(Route.Home)
+ await expect(page.getByText('Global Feed')).toHaveClass(/active/)
+ })
+
+ test('should display article when page loaded', async ({ page, conduit }) => {
+ await conduit.goto(Route.Home)
+ const articlePreview = page.getByTestId('article-preview').first()
+
+ await test.step('should have article preview', async () => {
+ await expect(articlePreview.getByRole('heading')).toContainText('abc123')
+ await expect(articlePreview.getByTestId('article-description')).toContainText('aaaaaaaaaaassssssssss')
+ })
+
+ await test.step('should redirect to article details page when click read more', async () => {
+ await articlePreview.getByText('Read more...').click()
+
+ await expect(page).toHaveURL(/#\/article\/.+/)
+ })
+ })
+
+ test('should jump to next page when click page 2 in pagination', async ({ page, conduit }) => {
+ await conduit.goto(Route.Home)
+
+ const waitForGetArticles = await conduit.intercept('GET', /articles\?limit=10&offset=10/, { fixture: 'articles.json' })
+
+ const [response] = await Promise.all([
+ waitForGetArticles(),
+ page.getByRole('link', { name: 'Go to page 2', exact: true }).click(),
+ ])
+
+ expect(response.request().url()).toContain('limit=10&offset=10')
+ })
+})
diff --git a/playwright/specs/user.spec.ts b/playwright/specs/user.spec.ts
new file mode 100644
index 00000000..15cf42f7
--- /dev/null
+++ b/playwright/specs/user.spec.ts
@@ -0,0 +1,52 @@
+import type { Article, Profile } from 'src/services/api.ts'
+import { Route } from '../constant'
+import { expect, test } from '../extends'
+import { ArticleDetailPageObject } from '../page-objects/article-detail.page-object.ts'
+
+test.beforeEach(async ({ conduit }) => {
+ await conduit.intercept('GET', /articles\?/, { fixture: 'articles.json' })
+ await conduit.intercept('GET', /tags/, { fixture: 'tags.json' })
+ await conduit.intercept('GET', /profiles\/\S+/, { fixture: 'profile.json' })
+})
+
+test.describe('follow', () => {
+ test.beforeEach(async ({ conduit }) => {
+ await conduit.intercept('GET', /articles\/\S+/, {
+ statusCode: 200,
+ fixture: 'article.json',
+ postFixture: (article: { article: Article }) => {
+ article.article.author.username = 'foo'
+ },
+ })
+ })
+
+ for (const [index, position] of (['banner', 'article footer'] as const).entries()) {
+ test(`should call follow user api when click ${position} follow user button`, async ({ page, conduit }) => {
+ await conduit.login()
+ await conduit.goto(Route.ArticleDetail)
+ const articlePage = new ArticleDetailPageObject(page)
+
+ const waitForFollowUser = await conduit.intercept('POST', /profiles\/\S+\/follow/, {
+ statusCode: 200,
+ fixture: 'profile.json',
+ postFixture: (profile: { profile: Profile }) => {
+ profile.profile.following = true
+ },
+ })
+
+ await Promise.all([
+ waitForFollowUser(),
+ articlePage.clickFollowUser(position),
+ ])
+
+ await expect(page.getByRole('button', { name: 'Unfollow' }).nth(index)).toBeVisible()
+ })
+ }
+
+ test('should not display follow button when user not logged', async ({ page, conduit }) => {
+ await conduit.goto(Route.ArticleDetail)
+
+ await expect(page.getByRole('heading', { name: 'Article body' })).toBeVisible()
+ await expect(page.getByRole('button', { name: 'Follow' })).not.toBeVisible()
+ })
+})
diff --git a/playwright/tsconfig.json b/playwright/tsconfig.json
new file mode 100644
index 00000000..baa0ff19
--- /dev/null
+++ b/playwright/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "composite": true,
+ "target": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "baseUrl": "..",
+ "paths": {
+ "src/*": ["./src/*"],
+ "page-objects/*": ["./playwright/page-objects/*"]
+ },
+ "noEmit": false,
+ "isolatedModules": false
+ },
+ "include": [
+ "./**/*"
+ ]
+}
diff --git a/playwright/utils/prettify.ts b/playwright/utils/prettify.ts
new file mode 100644
index 00000000..5278258d
--- /dev/null
+++ b/playwright/utils/prettify.ts
@@ -0,0 +1,19 @@
+import { prettyPrint } from 'html'
+
+export function formatHTML(rawHTMLString: string): string {
+ let removeComments = rawHTMLString;
+ let prev;
+ do {
+ prev = removeComments;
+ removeComments = removeComments.replaceAll(//gs, '');
+ } while (removeComments !== prev);
+ // eslint-disable-next-line camelcase
+ const pretty = prettyPrint(removeComments, { indent_size: 2 })
+ const removeEmptyLines = `${pretty}\n`.replaceAll(/\n{2,}/g, '\n')
+ return removeEmptyLines
+}
+
+export function formatJSON(json: string | object): string {
+ const jsonObject = typeof json === 'string' ? JSON.parse(json) as object : json
+ return JSON.stringify(jsonObject, null, 2)
+}
diff --git a/playwright/utils/test-decorators.ts b/playwright/utils/test-decorators.ts
new file mode 100644
index 00000000..4a222d6b
--- /dev/null
+++ b/playwright/utils/test-decorators.ts
@@ -0,0 +1,22 @@
+/* eslint-disable ts/no-unsafe-function-type,ts/no-unsafe-return */
+import { test } from '../extends'
+
+export function step(target: Function, context: ClassMethodDecoratorContext) {
+ return async function replacementMethod(this: Function, ...args: unknown[]) {
+ const className = this.constructor.name
+ const name = `${className.replace(/PageObject$/, '')}.${context.name as string}`
+ return await test.step(name, async () => {
+ return await target.call(this, ...args)
+ })
+ }
+}
+
+export function boxedStep(target: Function, context: ClassMethodDecoratorContext) {
+ return async function replacementMethod(this: Function, ...args: unknown[]) {
+ const className = this.constructor.name
+ const name = `${className.replace(/PageObject$/, '')}.${context.name as string}`
+ return await test.step(name, async () => {
+ return await target.call(this, ...args)
+ }, { box: true })
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e2374fd4..30f32161 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -26,10 +26,13 @@ importers:
devDependencies:
'@mutoe/eslint-config':
specifier: ^4.11.0-2
- version: 4.11.0-2(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(@vue/compiler-sfc@3.5.22)(eslint-import-resolver-node@0.3.9)(eslint-plugin-vuejs-accessibility@2.4.1(eslint@9.36.0(jiti@2.6.0)))(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.17)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@20.19.17)(typescript@5.9.2))(yaml@2.8.1))
+ version: 4.11.0-2(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(@vue/compiler-sfc@3.5.22)(eslint-import-resolver-node@0.3.9)(eslint-plugin-vuejs-accessibility@2.4.1(eslint@9.36.0(jiti@2.6.0)))(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(yaml@2.8.1))
'@pinia/testing':
specifier: ^1.0.2
version: 1.0.2(pinia@3.0.3(typescript@5.9.2)(vue@3.5.22(typescript@5.9.2)))
+ '@playwright/test':
+ specifier: ^1.55.1
+ version: 1.55.1
'@testing-library/cypress':
specifier: ^10.1.0
version: 10.1.0(cypress@13.13.2)
@@ -39,15 +42,24 @@ importers:
'@testing-library/vue':
specifier: ^8.1.0
version: 8.1.0(@vue/compiler-sfc@3.5.22)(@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.2)))(vue@3.5.22(typescript@5.9.2))
+ '@types/html':
+ specifier: ^1.0.4
+ version: 1.0.4
+ '@types/node':
+ specifier: ^24.5.2
+ version: 24.5.2
'@vitejs/plugin-vue':
specifier: ^6.0.1
- version: 6.0.1(vite@7.1.7(@types/node@20.19.17)(jiti@2.6.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.2))
+ version: 6.0.1(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.2))
'@vitest/coverage-v8':
specifier: ^3.2.4
- version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.17)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@20.19.17)(typescript@5.9.2))(yaml@2.8.1))
+ version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(yaml@2.8.1))
concurrently:
specifier: ^9.2.1
version: 9.2.1
+ cross-env:
+ specifier: ^7.0.3
+ version: 7.0.3
cypress:
specifier: ^13.13.2
version: 13.13.2
@@ -57,15 +69,21 @@ importers:
eslint-plugin-cypress:
specifier: ^5.1.1
version: 5.1.1(eslint@9.36.0(jiti@2.6.0))
+ eslint-plugin-vuejs-accessibility:
+ specifier: ^2.4.1
+ version: 2.4.1(eslint@9.36.0(jiti@2.6.0))
happy-dom:
specifier: ^18.0.1
version: 18.0.1
+ html:
+ specifier: ^1.0.0
+ version: 1.0.0
lint-staged:
specifier: ^16.2.0
version: 16.2.0
msw:
specifier: ^2.11.3
- version: 2.11.3(@types/node@20.19.17)(typescript@5.9.2)
+ version: 2.11.3(@types/node@24.5.2)(typescript@5.9.2)
rollup-plugin-analyzer:
specifier: ^4.0.0
version: 4.0.0
@@ -80,13 +98,13 @@ importers:
version: 5.9.2
vite:
specifier: ^7.1.7
- version: 7.1.7(@types/node@20.19.17)(jiti@2.6.0)(yaml@2.8.1)
+ version: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(yaml@2.8.1)
vitest:
specifier: ^3.2.4
- version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.17)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@20.19.17)(typescript@5.9.2))(yaml@2.8.1)
+ version: 3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(yaml@2.8.1)
vitest-dom:
specifier: ^0.1.1
- version: 0.1.1(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.17)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@20.19.17)(typescript@5.9.2))(yaml@2.8.1))
+ version: 0.1.1(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(yaml@2.8.1))
vue-tsc:
specifier: ^3.0.8
version: 3.0.8(typescript@5.9.2)
@@ -124,6 +142,10 @@ packages:
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-validator-identifier@7.24.7':
+ resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-validator-identifier@7.27.1':
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
engines: {node: '>=6.9.0'}
@@ -373,12 +395,22 @@ packages:
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0
+ '@eslint-community/eslint-utils@4.4.0':
+ resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
'@eslint-community/eslint-utils@4.9.0':
resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+ '@eslint-community/regexpp@4.10.0':
+ resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
'@eslint-community/regexpp@4.12.1':
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
@@ -613,6 +645,11 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
+ '@playwright/test@1.55.1':
+ resolution: {integrity: sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==}
+ engines: {node: '>=18'}
+ hasBin: true
+
'@rolldown/pluginutils@1.0.0-beta.29':
resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==}
@@ -780,9 +817,15 @@ packages:
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+ '@types/estree@1.0.5':
+ resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
+
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+ '@types/html@1.0.4':
+ resolution: {integrity: sha512-Wb1ymSAftCLxhc3D6vS0Ike/0xg7W6c+DQxAkerU6pD7C8CMzTYwvrwnlcrTfsVO/nMelB9KOKIT7+N5lOeQUg==}
+
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -798,8 +841,8 @@ packages:
'@types/node@20.19.17':
resolution: {integrity: sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==}
- '@types/node@22.1.0':
- resolution: {integrity: sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==}
+ '@types/node@24.5.2':
+ resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==}
'@types/normalize-package-data@2.4.4':
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
@@ -1127,6 +1170,11 @@ packages:
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+ acorn@8.12.1:
+ resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
acorn@8.15.0:
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
engines: {node: '>=0.4.0'}
@@ -1198,10 +1246,6 @@ packages:
aria-query@5.3.0:
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
- aria-query@5.3.2:
- resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
- engines: {node: '>= 0.4'}
-
array-buffer-byte-length@1.0.0:
resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==}
@@ -1289,6 +1333,9 @@ packages:
buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
+ buffer-from@1.1.2:
+ resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
@@ -1452,6 +1499,10 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+ concat-stream@1.6.2:
+ resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
+ engines: {'0': node >= 0.8}
+
concurrently@9.2.1:
resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==}
engines: {node: '>=18'}
@@ -1484,6 +1535,11 @@ packages:
core-util-is@1.0.2:
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
+ cross-env@7.0.3:
+ resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
+ engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
+ hasBin: true
+
cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
@@ -1649,6 +1705,10 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ escalade@3.1.2:
+ resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
+ engines: {node: '>=6'}
+
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -2019,6 +2079,11 @@ packages:
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
engines: {node: '>=10'}
+ fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -2172,6 +2237,10 @@ packages:
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
+ html@1.0.0:
+ resolution: {integrity: sha512-lw/7YsdKiP3kk5PnR1INY17iJuzdAtJewxr14ozKJWbbR97znovZ0mh+WEMZ8rjc3lgTK+ID/htTjuyGKB52Kw==}
+ hasBin: true
+
http-signature@1.3.6:
resolution: {integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==}
engines: {node: '>=0.10'}
@@ -2186,6 +2255,10 @@ packages:
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+ ignore@5.3.1:
+ resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
+ engines: {node: '>= 4'}
+
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -2214,6 +2287,9 @@ packages:
resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==}
engines: {node: '>=18'}
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
@@ -2342,6 +2418,9 @@ packages:
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
engines: {node: '>=12.13'}
+ isarray@1.0.0:
+ resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
+
isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
@@ -2396,6 +2475,10 @@ packages:
jsbn@0.1.1:
resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
+ jsdoc-type-pratt-parser@4.0.0:
+ resolution: {integrity: sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==}
+ engines: {node: '>=12.0.0'}
+
jsdoc-type-pratt-parser@4.1.0:
resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==}
engines: {node: '>=12.0.0'}
@@ -2993,6 +3076,16 @@ packages:
pkg-types@2.3.0:
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
+ playwright-core@1.55.1:
+ resolution: {integrity: sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.55.1:
+ resolution: {integrity: sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==}
+ engines: {node: '>=18'}
+ hasBin: true
+
pluralize@8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
@@ -3020,6 +3113,9 @@ packages:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+ process-nextick-args@2.0.1:
+ resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
+
process@0.11.10:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'}
@@ -3067,6 +3163,9 @@ packages:
resolution: {integrity: sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==}
engines: {node: '>=18'}
+ readable-stream@2.3.8:
+ resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
+
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
@@ -3161,6 +3260,9 @@ packages:
rxjs@7.8.2:
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
+ safe-buffer@5.1.2:
+ resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
@@ -3320,6 +3422,9 @@ packages:
resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==}
engines: {node: '>=20'}
+ string_decoder@1.1.1:
+ resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
+
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@@ -3492,6 +3597,9 @@ packages:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
+ typedarray@0.0.6:
+ resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
+
typescript@5.9.2:
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
engines: {node: '>=14.17'}
@@ -3500,12 +3608,12 @@ packages:
ufo@1.6.1:
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
- undici-types@6.13.0:
- resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==}
-
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+ undici-types@7.12.0:
+ resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==}
+
unicorn-magic@0.1.0:
resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==}
engines: {node: '>=18'}
@@ -3809,6 +3917,8 @@ snapshots:
'@babel/helper-validator-identifier@7.22.20': {}
+ '@babel/helper-validator-identifier@7.24.7': {}
+
'@babel/helper-validator-identifier@7.27.1': {}
'@babel/highlight@7.23.4':
@@ -3832,7 +3942,7 @@ snapshots:
'@babel/types@7.25.2':
dependencies:
'@babel/helper-string-parser': 7.24.8
- '@babel/helper-validator-identifier': 7.27.1
+ '@babel/helper-validator-identifier': 7.24.7
to-fast-properties: 2.0.0
'@babel/types@7.28.4':
@@ -4004,13 +4114,20 @@ snapshots:
dependencies:
escape-string-regexp: 4.0.0
eslint: 9.36.0(jiti@2.6.0)
- ignore: 5.3.2
+ ignore: 5.3.1
+
+ '@eslint-community/eslint-utils@4.4.0(eslint@9.36.0(jiti@2.6.0))':
+ dependencies:
+ eslint: 9.36.0(jiti@2.6.0)
+ eslint-visitor-keys: 3.4.3
'@eslint-community/eslint-utils@4.9.0(eslint@9.36.0(jiti@2.6.0))':
dependencies:
eslint: 9.36.0(jiti@2.6.0)
eslint-visitor-keys: 3.4.3
+ '@eslint-community/regexpp@4.10.0': {}
+
'@eslint-community/regexpp@4.12.1': {}
'@eslint/compat@1.4.0(eslint@9.36.0(jiti@2.6.0))':
@@ -4022,7 +4139,7 @@ snapshots:
'@eslint/config-array@0.21.0':
dependencies:
'@eslint/object-schema': 2.1.6
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.3.6(supports-color@8.1.1)
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@@ -4044,10 +4161,10 @@ snapshots:
'@eslint/eslintrc@3.3.1':
dependencies:
ajv: 6.12.6
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.3.6(supports-color@8.1.1)
espree: 10.4.0
globals: 14.0.0
- ignore: 5.3.2
+ ignore: 5.3.1
import-fresh: 3.3.0
js-yaml: 4.1.0
minimatch: 3.1.2
@@ -4092,31 +4209,31 @@ snapshots:
'@inquirer/ansi@1.0.0': {}
- '@inquirer/confirm@5.1.18(@types/node@20.19.17)':
+ '@inquirer/confirm@5.1.18(@types/node@24.5.2)':
dependencies:
- '@inquirer/core': 10.2.2(@types/node@20.19.17)
- '@inquirer/type': 3.0.8(@types/node@20.19.17)
+ '@inquirer/core': 10.2.2(@types/node@24.5.2)
+ '@inquirer/type': 3.0.8(@types/node@24.5.2)
optionalDependencies:
- '@types/node': 20.19.17
+ '@types/node': 24.5.2
- '@inquirer/core@10.2.2(@types/node@20.19.17)':
+ '@inquirer/core@10.2.2(@types/node@24.5.2)':
dependencies:
'@inquirer/ansi': 1.0.0
'@inquirer/figures': 1.0.13
- '@inquirer/type': 3.0.8(@types/node@20.19.17)
+ '@inquirer/type': 3.0.8(@types/node@24.5.2)
cli-width: 4.1.0
mute-stream: 2.0.0
signal-exit: 4.1.0
wrap-ansi: 6.2.0
yoctocolors-cjs: 2.1.2
optionalDependencies:
- '@types/node': 20.19.17
+ '@types/node': 24.5.2
'@inquirer/figures@1.0.13': {}
- '@inquirer/type@3.0.8(@types/node@20.19.17)':
+ '@inquirer/type@3.0.8(@types/node@24.5.2)':
optionalDependencies:
- '@types/node': 20.19.17
+ '@types/node': 24.5.2
'@isaacs/cliui@8.0.2':
dependencies:
@@ -4162,7 +4279,7 @@ snapshots:
outvariant: 1.4.3
strict-event-emitter: 0.5.1
- '@mutoe/eslint-config@4.11.0-2(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(@vue/compiler-sfc@3.5.22)(eslint-import-resolver-node@0.3.9)(eslint-plugin-vuejs-accessibility@2.4.1(eslint@9.36.0(jiti@2.6.0)))(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.17)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@20.19.17)(typescript@5.9.2))(yaml@2.8.1))':
+ '@mutoe/eslint-config@4.11.0-2(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(@vue/compiler-sfc@3.5.22)(eslint-import-resolver-node@0.3.9)(eslint-plugin-vuejs-accessibility@2.4.1(eslint@9.36.0(jiti@2.6.0)))(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(yaml@2.8.1))':
dependencies:
'@antfu/install-pkg': 1.1.0
'@clack/prompts': 0.10.1
@@ -4172,7 +4289,7 @@ snapshots:
'@stylistic/eslint-plugin': 4.4.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)
'@typescript-eslint/eslint-plugin': 8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)
'@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)
- '@vitest/eslint-plugin': 1.3.12(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.17)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@20.19.17)(typescript@5.9.2))(yaml@2.8.1))
+ '@vitest/eslint-plugin': 1.3.12(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(yaml@2.8.1))
ansis: 3.17.0
cac: 6.7.14
eslint: 9.36.0(jiti@2.6.0)
@@ -4262,6 +4379,10 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
+ '@playwright/test@1.55.1':
+ dependencies:
+ playwright: 1.55.1
+
'@rolldown/pluginutils@1.0.0-beta.29': {}
'@rollup/rollup-android-arm-eabi@4.52.2':
@@ -4350,7 +4471,7 @@ snapshots:
'@testing-library/dom@10.4.0':
dependencies:
- '@babel/code-frame': 7.27.1
+ '@babel/code-frame': 7.23.4
'@babel/runtime': 7.23.4
'@types/aria-query': 5.0.4
aria-query: 5.3.0
@@ -4404,8 +4525,12 @@ snapshots:
'@types/deep-eql@4.0.2': {}
+ '@types/estree@1.0.5': {}
+
'@types/estree@1.0.8': {}
+ '@types/html@1.0.4': {}
+
'@types/json-schema@7.0.15': {}
'@types/lodash@4.17.20': {}
@@ -4420,10 +4545,9 @@ snapshots:
dependencies:
undici-types: 6.21.0
- '@types/node@22.1.0':
+ '@types/node@24.5.2':
dependencies:
- undici-types: 6.13.0
- optional: true
+ undici-types: 7.12.0
'@types/normalize-package-data@2.4.4': {}
@@ -4443,12 +4567,12 @@ snapshots:
'@types/yauzl@2.10.3':
dependencies:
- '@types/node': 22.1.0
+ '@types/node': 24.5.2
optional: true
'@typescript-eslint/eslint-plugin@8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)':
dependencies:
- '@eslint-community/regexpp': 4.12.1
+ '@eslint-community/regexpp': 4.10.0
'@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)
'@typescript-eslint/scope-manager': 8.44.1
'@typescript-eslint/type-utils': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)
@@ -4469,7 +4593,7 @@ snapshots:
'@typescript-eslint/types': 8.44.1
'@typescript-eslint/typescript-estree': 8.44.1(typescript@5.9.2)
'@typescript-eslint/visitor-keys': 8.44.1
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.3.6(supports-color@8.1.1)
eslint: 9.36.0(jiti@2.6.0)
typescript: 5.9.2
transitivePeerDependencies:
@@ -4479,7 +4603,7 @@ snapshots:
dependencies:
'@typescript-eslint/tsconfig-utils': 8.44.1(typescript@5.9.2)
'@typescript-eslint/types': 8.44.1
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.3.6(supports-color@8.1.1)
typescript: 5.9.2
transitivePeerDependencies:
- supports-color
@@ -4498,7 +4622,7 @@ snapshots:
'@typescript-eslint/types': 8.44.1
'@typescript-eslint/typescript-estree': 8.44.1(typescript@5.9.2)
'@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.3.6(supports-color@8.1.1)
eslint: 9.36.0(jiti@2.6.0)
ts-api-utils: 2.1.0(typescript@5.9.2)
typescript: 5.9.2
@@ -4513,11 +4637,11 @@ snapshots:
'@typescript-eslint/tsconfig-utils': 8.44.1(typescript@5.9.2)
'@typescript-eslint/types': 8.44.1
'@typescript-eslint/visitor-keys': 8.44.1
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.3.6(supports-color@8.1.1)
fast-glob: 3.3.2
is-glob: 4.0.3
minimatch: 9.0.5
- semver: 7.7.2
+ semver: 7.6.3
ts-api-utils: 2.1.0(typescript@5.9.2)
typescript: 5.9.2
transitivePeerDependencies:
@@ -4598,18 +4722,18 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
- '@vitejs/plugin-vue@6.0.1(vite@7.1.7(@types/node@20.19.17)(jiti@2.6.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.2))':
+ '@vitejs/plugin-vue@6.0.1(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.2))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.29
- vite: 7.1.7(@types/node@20.19.17)(jiti@2.6.0)(yaml@2.8.1)
+ vite: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(yaml@2.8.1)
vue: 3.5.22(typescript@5.9.2)
- '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.17)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@20.19.17)(typescript@5.9.2))(yaml@2.8.1))':
+ '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(yaml@2.8.1))':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
ast-v8-to-istanbul: 0.3.5
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.4.3
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-lib-source-maps: 5.0.6
@@ -4619,18 +4743,18 @@ snapshots:
std-env: 3.9.0
test-exclude: 7.0.1
tinyrainbow: 2.0.0
- vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.17)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@20.19.17)(typescript@5.9.2))(yaml@2.8.1)
+ vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(yaml@2.8.1)
transitivePeerDependencies:
- supports-color
- '@vitest/eslint-plugin@1.3.12(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.17)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@20.19.17)(typescript@5.9.2))(yaml@2.8.1))':
+ '@vitest/eslint-plugin@1.3.12(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(yaml@2.8.1))':
dependencies:
'@typescript-eslint/scope-manager': 8.44.1
'@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2)
eslint: 9.36.0(jiti@2.6.0)
optionalDependencies:
typescript: 5.9.2
- vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.17)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@20.19.17)(typescript@5.9.2))(yaml@2.8.1)
+ vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(yaml@2.8.1)
transitivePeerDependencies:
- supports-color
@@ -4642,14 +4766,14 @@ snapshots:
chai: 5.3.3
tinyrainbow: 2.0.0
- '@vitest/mocker@3.2.4(msw@2.11.3(@types/node@20.19.17)(typescript@5.9.2))(vite@7.1.7(@types/node@20.19.17)(jiti@2.6.0)(yaml@2.8.1))':
+ '@vitest/mocker@3.2.4(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(yaml@2.8.1))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.19
optionalDependencies:
- msw: 2.11.3(@types/node@20.19.17)(typescript@5.9.2)
- vite: 7.1.7(@types/node@20.19.17)(jiti@2.6.0)(yaml@2.8.1)
+ msw: 2.11.3(@types/node@24.5.2)(typescript@5.9.2)
+ vite: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(yaml@2.8.1)
'@vitest/pretty-format@3.2.4':
dependencies:
@@ -4766,7 +4890,7 @@ snapshots:
alien-signals: 2.0.7
muggle-string: 0.4.1
path-browserify: 1.0.1
- picomatch: 4.0.3
+ picomatch: 4.0.2
optionalDependencies:
typescript: 5.9.2
@@ -4806,10 +4930,16 @@ snapshots:
abbrev@2.0.0: {}
+ acorn-jsx@5.3.2(acorn@8.12.1):
+ dependencies:
+ acorn: 8.12.1
+
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.15.0
+ acorn@8.12.1: {}
+
acorn@8.15.0: {}
aggregate-error@3.1.0:
@@ -4868,9 +4998,6 @@ snapshots:
dependencies:
dequal: 2.0.3
- aria-query@5.3.2:
- optional: true
-
array-buffer-byte-length@1.0.0:
dependencies:
call-bind: 1.0.5
@@ -4947,6 +5074,8 @@ snapshots:
buffer-crc32@0.2.13: {}
+ buffer-from@1.1.2: {}
+
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
@@ -5098,6 +5227,13 @@ snapshots:
concat-map@0.0.1: {}
+ concat-stream@1.6.2:
+ dependencies:
+ buffer-from: 1.1.2
+ inherits: 2.0.4
+ readable-stream: 2.3.8
+ typedarray: 0.0.6
+
concurrently@9.2.1:
dependencies:
chalk: 4.1.2
@@ -5130,6 +5266,10 @@ snapshots:
core-util-is@1.0.2: {}
+ cross-env@7.0.3:
+ dependencies:
+ cross-spawn: 7.0.6
+
cross-spawn@7.0.3:
dependencies:
path-key: 3.1.1
@@ -5213,11 +5353,9 @@ snapshots:
optionalDependencies:
supports-color: 8.1.1
- debug@4.4.3(supports-color@8.1.1):
+ debug@4.4.3:
dependencies:
ms: 2.1.3
- optionalDependencies:
- supports-color: 8.1.1
decode-named-character-reference@1.2.0:
dependencies:
@@ -5363,6 +5501,8 @@ snapshots:
'@esbuild/win32-ia32': 0.25.10
'@esbuild/win32-x64': 0.25.10
+ escalade@3.1.2: {}
+
escalade@3.2.0: {}
escape-string-regexp@1.0.5: {}
@@ -5374,12 +5514,12 @@ snapshots:
eslint-compat-utils@0.5.1(eslint@9.36.0(jiti@2.6.0)):
dependencies:
eslint: 9.36.0(jiti@2.6.0)
- semver: 7.7.2
+ semver: 7.6.3
eslint-compat-utils@0.6.5(eslint@9.36.0(jiti@2.6.0)):
dependencies:
eslint: 9.36.0(jiti@2.6.0)
- semver: 7.7.2
+ semver: 7.6.3
eslint-config-flat-gitignore@2.1.0(eslint@9.36.0(jiti@2.6.0)):
dependencies:
@@ -5441,7 +5581,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 8.44.1
comment-parser: 1.4.1
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.4.3
eslint: 9.36.0(jiti@2.6.0)
eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
is-glob: 4.0.3
@@ -5460,7 +5600,7 @@ snapshots:
'@es-joy/jsdoccomment': 0.50.2
are-docs-informative: 0.0.2
comment-parser: 1.4.1
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.4.3
escape-string-regexp: 4.0.0
eslint: 9.36.0(jiti@2.6.0)
espree: 10.4.0
@@ -5477,7 +5617,7 @@ snapshots:
eslint: 9.36.0(jiti@2.6.0)
eslint-compat-utils: 0.6.5(eslint@9.36.0(jiti@2.6.0))
eslint-json-compat-utils: 0.2.1(eslint@9.36.0(jiti@2.6.0))(jsonc-eslint-parser@2.4.0)
- espree: 10.4.0
+ espree: 9.6.1
graphemer: 1.4.0
jsonc-eslint-parser: 2.4.0
natural-compare: 1.4.0
@@ -5495,7 +5635,7 @@ snapshots:
globals: 15.15.0
globrex: 0.1.2
ignore: 5.3.2
- semver: 7.7.2
+ semver: 7.6.3
ts-declaration-location: 1.0.7(typescript@5.9.2)
transitivePeerDependencies:
- typescript
@@ -5524,18 +5664,18 @@ snapshots:
eslint-plugin-regexp@2.10.0(eslint@9.36.0(jiti@2.6.0)):
dependencies:
- '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.0))
+ '@eslint-community/eslint-utils': 4.4.0(eslint@9.36.0(jiti@2.6.0))
'@eslint-community/regexpp': 4.12.1
comment-parser: 1.4.1
eslint: 9.36.0(jiti@2.6.0)
- jsdoc-type-pratt-parser: 4.1.0
+ jsdoc-type-pratt-parser: 4.0.0
refa: 0.12.1
regexp-ast-analysis: 0.7.1
scslre: 0.3.0
eslint-plugin-toml@0.12.0(eslint@9.36.0(jiti@2.6.0)):
dependencies:
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.3.6(supports-color@8.1.1)
eslint: 9.36.0(jiti@2.6.0)
eslint-compat-utils: 0.6.5(eslint@9.36.0(jiti@2.6.0))
lodash: 4.17.21
@@ -5571,12 +5711,12 @@ snapshots:
eslint-plugin-vue@10.5.0(@stylistic/eslint-plugin@4.4.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.9.2))(eslint@9.36.0(jiti@2.6.0))(vue-eslint-parser@10.2.0(eslint@9.36.0(jiti@2.6.0))):
dependencies:
- '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.0))
+ '@eslint-community/eslint-utils': 4.4.0(eslint@9.36.0(jiti@2.6.0))
eslint: 9.36.0(jiti@2.6.0)
natural-compare: 1.4.0
nth-check: 2.1.1
postcss-selector-parser: 6.1.1
- semver: 7.7.2
+ semver: 7.6.3
vue-eslint-parser: 10.2.0(eslint@9.36.0(jiti@2.6.0))
xml-name-validator: 4.0.0
optionalDependencies:
@@ -5585,17 +5725,16 @@ snapshots:
eslint-plugin-vuejs-accessibility@2.4.1(eslint@9.36.0(jiti@2.6.0)):
dependencies:
- aria-query: 5.3.2
+ aria-query: 5.3.0
emoji-regex: 10.5.0
eslint: 9.36.0(jiti@2.6.0)
vue-eslint-parser: 9.4.3(eslint@9.36.0(jiti@2.6.0))
transitivePeerDependencies:
- supports-color
- optional: true
eslint-plugin-yml@1.18.0(eslint@9.36.0(jiti@2.6.0)):
dependencies:
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.3.6(supports-color@8.1.1)
escape-string-regexp: 4.0.0
eslint: 9.36.0(jiti@2.6.0)
eslint-compat-utils: 0.6.5(eslint@9.36.0(jiti@2.6.0))
@@ -5613,7 +5752,6 @@ snapshots:
dependencies:
esrecurse: 4.3.0
estraverse: 5.3.0
- optional: true
eslint-scope@8.4.0:
dependencies:
@@ -5642,7 +5780,7 @@ snapshots:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.3.6(supports-color@8.1.1)
escape-string-regexp: 4.0.0
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
@@ -5653,7 +5791,7 @@ snapshots:
file-entry-cache: 8.0.0
find-up: 5.0.0
glob-parent: 6.0.2
- ignore: 5.3.2
+ ignore: 5.3.1
imurmurhash: 0.1.4
is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1
@@ -5674,8 +5812,8 @@ snapshots:
espree@9.6.1:
dependencies:
- acorn: 8.15.0
- acorn-jsx: 5.3.2(acorn@8.15.0)
+ acorn: 8.12.1
+ acorn-jsx: 5.3.2(acorn@8.12.1)
eslint-visitor-keys: 3.4.3
esquery@1.6.0:
@@ -5692,7 +5830,7 @@ snapshots:
estree-walker@3.0.3:
dependencies:
- '@types/estree': 1.0.8
+ '@types/estree': 1.0.5
esutils@2.0.3: {}
@@ -5726,7 +5864,7 @@ snapshots:
extract-zip@2.0.1(supports-color@8.1.1):
dependencies:
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.3.6(supports-color@8.1.1)
get-stream: 5.2.0
yauzl: 2.10.0
optionalDependencies:
@@ -5802,7 +5940,7 @@ snapshots:
foreground-child@3.1.1:
dependencies:
- cross-spawn: 7.0.3
+ cross-spawn: 7.0.6
signal-exit: 4.1.0
forever-agent@0.6.1: {}
@@ -5822,6 +5960,9 @@ snapshots:
jsonfile: 6.1.0
universalify: 2.0.1
+ fsevents@2.3.2:
+ optional: true
+
fsevents@2.3.3:
optional: true
@@ -5960,6 +6101,10 @@ snapshots:
html-escaper@2.0.2: {}
+ html@1.0.0:
+ dependencies:
+ concat-stream: 1.6.2
+
http-signature@1.3.6:
dependencies:
assert-plus: 1.0.0
@@ -5972,6 +6117,8 @@ snapshots:
ieee754@1.2.1: {}
+ ignore@5.3.1: {}
+
ignore@5.3.2: {}
ignore@7.0.5: {}
@@ -5989,6 +6136,8 @@ snapshots:
index-to-position@1.2.0: {}
+ inherits@2.0.4: {}
+
ini@1.3.8: {}
ini@2.0.0: {}
@@ -6110,6 +6259,8 @@ snapshots:
is-what@4.1.16: {}
+ isarray@1.0.0: {}
+
isarray@2.0.5: {}
isexe@2.0.0: {}
@@ -6127,7 +6278,7 @@ snapshots:
istanbul-lib-source-maps@5.0.6:
dependencies:
'@jridgewell/trace-mapping': 0.3.25
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.4.3
istanbul-lib-coverage: 3.2.2
transitivePeerDependencies:
- supports-color
@@ -6168,6 +6319,8 @@ snapshots:
jsbn@0.1.1: {}
+ jsdoc-type-pratt-parser@4.0.0: {}
+
jsdoc-type-pratt-parser@4.1.0: {}
jsesc@3.0.2: {}
@@ -6186,10 +6339,10 @@ snapshots:
jsonc-eslint-parser@2.4.0:
dependencies:
- acorn: 8.15.0
+ acorn: 8.12.1
eslint-visitor-keys: 3.4.3
espree: 9.6.1
- semver: 7.7.2
+ semver: 7.6.3
jsonfile@6.1.0:
dependencies:
@@ -6311,7 +6464,7 @@ snapshots:
make-dir@4.0.0:
dependencies:
- semver: 7.7.2
+ semver: 7.6.3
markdown-table@3.0.4: {}
@@ -6613,7 +6766,7 @@ snapshots:
micromark@4.0.2:
dependencies:
'@types/debug': 4.1.12
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.3.6(supports-color@8.1.1)
decode-named-character-reference: 1.2.0
devlop: 1.1.0
micromark-core-commonmark: 2.0.3
@@ -6685,11 +6838,11 @@ snapshots:
ms@2.1.3: {}
- msw@2.11.3(@types/node@20.19.17)(typescript@5.9.2):
+ msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2):
dependencies:
'@bundled-es-modules/cookie': 2.0.1
'@bundled-es-modules/statuses': 1.0.1
- '@inquirer/confirm': 5.1.18(@types/node@20.19.17)
+ '@inquirer/confirm': 5.1.18(@types/node@24.5.2)
'@mswjs/interceptors': 0.39.6
'@open-draft/deferred-promise': 2.2.0
'@types/cookie': 0.6.0
@@ -6945,6 +7098,14 @@ snapshots:
exsolve: 1.0.7
pathe: 2.0.3
+ playwright-core@1.55.1: {}
+
+ playwright@1.55.1:
+ dependencies:
+ playwright-core: 1.55.1
+ optionalDependencies:
+ fsevents: 2.3.2
+
pluralize@8.0.0: {}
pnpm-workspace-yaml@0.3.1:
@@ -6972,6 +7133,8 @@ snapshots:
ansi-styles: 5.2.0
react-is: 17.0.2
+ process-nextick-args@2.0.1: {}
+
process@0.11.10: {}
proto-list@1.2.4: {}
@@ -7018,6 +7181,16 @@ snapshots:
type-fest: 4.24.0
unicorn-magic: 0.1.0
+ readable-stream@2.3.8:
+ dependencies:
+ core-util-is: 1.0.2
+ inherits: 2.0.4
+ isarray: 1.0.0
+ process-nextick-args: 2.0.1
+ safe-buffer: 5.1.2
+ string_decoder: 1.1.1
+ util-deprecate: 1.0.2
+
readdirp@4.1.2: {}
redent@4.0.0:
@@ -7129,6 +7302,8 @@ snapshots:
dependencies:
tslib: 2.6.2
+ safe-buffer@5.1.2: {}
+
safe-buffer@5.2.1: {}
safer-buffer@2.1.2: {}
@@ -7297,6 +7472,10 @@ snapshots:
get-east-asian-width: 1.4.0
strip-ansi: 7.1.0
+ string_decoder@1.1.1:
+ dependencies:
+ safe-buffer: 5.1.2
+
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
@@ -7470,15 +7649,16 @@ snapshots:
type-fest@4.41.0: {}
+ typedarray@0.0.6: {}
+
typescript@5.9.2: {}
ufo@1.6.1: {}
- undici-types@6.13.0:
- optional: true
-
undici-types@6.21.0: {}
+ undici-types@7.12.0: {}
+
unicorn-magic@0.1.0: {}
unist-util-is@6.0.0:
@@ -7562,13 +7742,13 @@ snapshots:
core-util-is: 1.0.2
extsprintf: 1.3.0
- vite-node@3.2.4(@types/node@20.19.17)(jiti@2.6.0)(yaml@2.8.1):
+ vite-node@3.2.4(@types/node@24.5.2)(jiti@2.6.0)(yaml@2.8.1):
dependencies:
cac: 6.7.14
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
- vite: 7.1.7(@types/node@20.19.17)(jiti@2.6.0)(yaml@2.8.1)
+ vite: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(yaml@2.8.1)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -7583,7 +7763,7 @@ snapshots:
- tsx
- yaml
- vite@7.1.7(@types/node@20.19.17)(jiti@2.6.0)(yaml@2.8.1):
+ vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(yaml@2.8.1):
dependencies:
esbuild: 0.25.10
fdir: 6.5.0(picomatch@4.0.3)
@@ -7592,12 +7772,12 @@ snapshots:
rollup: 4.52.2
tinyglobby: 0.2.15
optionalDependencies:
- '@types/node': 20.19.17
+ '@types/node': 24.5.2
fsevents: 2.3.3
jiti: 2.6.0
yaml: 2.8.1
- vitest-dom@0.1.1(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.17)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@20.19.17)(typescript@5.9.2))(yaml@2.8.1)):
+ vitest-dom@0.1.1(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(yaml@2.8.1)):
dependencies:
aria-query: 5.3.0
chalk: 5.3.0
@@ -7605,36 +7785,36 @@ snapshots:
dom-accessibility-api: 0.6.3
lodash-es: 4.17.21
redent: 4.0.0
- vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.17)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@20.19.17)(typescript@5.9.2))(yaml@2.8.1)
+ vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(yaml@2.8.1)
- vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.17)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@20.19.17)(typescript@5.9.2))(yaml@2.8.1):
+ vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.5.2)(happy-dom@18.0.1)(jiti@2.6.0)(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(yaml@2.8.1):
dependencies:
'@types/chai': 5.2.2
'@vitest/expect': 3.2.4
- '@vitest/mocker': 3.2.4(msw@2.11.3(@types/node@20.19.17)(typescript@5.9.2))(vite@7.1.7(@types/node@20.19.17)(jiti@2.6.0)(yaml@2.8.1))
+ '@vitest/mocker': 3.2.4(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(yaml@2.8.1))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
'@vitest/spy': 3.2.4
'@vitest/utils': 3.2.4
chai: 5.3.3
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.4.3
expect-type: 1.2.2
magic-string: 0.30.19
pathe: 2.0.3
- picomatch: 4.0.3
+ picomatch: 4.0.2
std-env: 3.9.0
tinybench: 2.9.0
tinyexec: 0.3.2
tinyglobby: 0.2.15
tinypool: 1.1.1
tinyrainbow: 2.0.0
- vite: 7.1.7(@types/node@20.19.17)(jiti@2.6.0)(yaml@2.8.1)
- vite-node: 3.2.4(@types/node@20.19.17)(jiti@2.6.0)(yaml@2.8.1)
+ vite: 7.1.7(@types/node@24.5.2)(jiti@2.6.0)(yaml@2.8.1)
+ vite-node: 3.2.4(@types/node@24.5.2)(jiti@2.6.0)(yaml@2.8.1)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/debug': 4.1.12
- '@types/node': 20.19.17
+ '@types/node': 24.5.2
happy-dom: 18.0.1
transitivePeerDependencies:
- jiti
@@ -7656,19 +7836,19 @@ snapshots:
vue-eslint-parser@10.2.0(eslint@9.36.0(jiti@2.6.0)):
dependencies:
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.4.3
eslint: 9.36.0(jiti@2.6.0)
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
espree: 10.4.0
esquery: 1.6.0
- semver: 7.7.2
+ semver: 7.6.3
transitivePeerDependencies:
- supports-color
vue-eslint-parser@9.4.3(eslint@9.36.0(jiti@2.6.0)):
dependencies:
- debug: 4.4.3(supports-color@8.1.1)
+ debug: 4.4.3
eslint: 9.36.0(jiti@2.6.0)
eslint-scope: 7.2.2
eslint-visitor-keys: 3.4.3
@@ -7678,7 +7858,6 @@ snapshots:
semver: 7.7.2
transitivePeerDependencies:
- supports-color
- optional: true
vue-router@4.5.1(vue@3.5.22(typescript@5.9.2)):
dependencies:
@@ -7788,7 +7967,7 @@ snapshots:
yargs@17.7.2:
dependencies:
cliui: 8.0.1
- escalade: 3.2.0
+ escalade: 3.1.2
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
new file mode 100644
index 00000000..eb37b315
--- /dev/null
+++ b/pnpm-workspace.yaml
@@ -0,0 +1,7 @@
+ignoredBuiltDependencies:
+ - msw
+ - unrs-resolver
+
+onlyBuiltDependencies:
+ - esbuild
+ - simple-git-hooks
diff --git a/qodana.yaml b/qodana.yaml
new file mode 100644
index 00000000..535cee4e
--- /dev/null
+++ b/qodana.yaml
@@ -0,0 +1,29 @@
+# -------------------------------------------------------------------------------#
+# Qodana analysis is configured by qodana.yaml file #
+# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
+# -------------------------------------------------------------------------------#
+version: '1.0'
+
+# Specify inspection profile for code analysis
+profile:
+ name: qodana.starter
+
+# Enable inspections
+# include:
+# - name:
+
+# Disable inspections
+# exclude:
+# - name:
+# paths:
+# -
+
+# Execute shell command before Qodana execution (Applied in CI/CD pipeline)
+# bootstrap: sh ./prepare-qodana.sh
+
+# Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
+# plugins:
+# - id: #(plugin id can be found at https://plugins.jetbrains.com)
+
+# Specify Qodana linter for analysis (Applied in CI/CD pipeline)
+linter: jetbrains/qodana-jvm:latest
diff --git a/src/components/AppPagination.vue b/src/components/AppPagination.vue
index c4e22423..3f20c6d4 100644
--- a/src/components/AppPagination.vue
+++ b/src/components/AppPagination.vue
@@ -1,5 +1,5 @@
-