Compare commits

...

3 Commits

Author SHA1 Message Date
Matiss Janis Aboltins
d796d8ec9b low res :( 2026-01-15 20:39:07 +00:00
Matiss Janis Aboltins
69009ce8ee Merge branch 'master' into matiss/browser-tests 2026-01-14 20:50:31 +00:00
Matiss Janis Aboltins
1e290373e5 Base 2026-01-14 20:49:51 +00:00
12 changed files with 672 additions and 202 deletions

29
bin/run-browser-tests Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/sh
# Run browser tests (vitest browser mode) in Docker for consistent screenshot quality
# Browser tests generate screenshots that vary by environment (fonts, rendering, etc.)
# Running in Docker ensures consistent results across different machines
#
# Usage:
# yarn test:browser # Run all browser tests
# yarn test:browser AuthSettings.browser # Run specific test file
# yarn test:browser --update # Update snapshots
if [ ! -d "node_modules" ] || [ "$(ls -A node_modules)" = "" ]; then
yarn
fi
TEST_ARGS=""
# Loop through all arguments
while [ $# -gt 0 ]; do
TEST_ARGS="$TEST_ARGS $1"
shift
done
echo "Running browser tests in Docker for consistent screenshot quality..."
echo "Test args: $TEST_ARGS"
echo ""
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.57.0-jammy /bin/bash \
-c "yarn workspace @actual-app/web test --project=browser $TEST_ARGS"

View File

@@ -44,6 +44,7 @@
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
"generate:release-notes": "ts-node ./bin/release-note-generator.ts",
"test": "lage test --continue",
"test:browser": "./bin/run-browser-tests",
"test:debug": "lage test --no-cache --continue",
"e2e": "yarn workspace @actual-app/web run e2e",
"e2e:desktop": "yarn build:desktop --skip-exe-build --skip-translations && yarn workspace desktop-electron e2e",

View File

@@ -8,6 +8,7 @@ coverage
test-results
playwright-report
blob-report
.vitest-attachments/
# production
build

View File

@@ -96,3 +96,48 @@ Run locally:
```sh
E2E_START_URL=https://my-remote-server.com yarn vrt
```
## Browser Tests (Vitest Browser Mode)
Browser tests (`.browser.test.tsx` files) use Vitest's browser mode to test React components with visual regression screenshots. These tests generate screenshots that can vary significantly by environment (fonts, rendering, DPI, etc.).
**IMPORTANT: For consistent screenshot quality, always run browser tests in Docker.**
### Running Browser Tests in Docker
From the project root:
```sh
# Run all browser tests
yarn test:browser:docker
# Run a specific browser test file
yarn test:browser:docker AuthSettings.browser
# Run with update flag to update snapshots
yarn test:browser:docker AuthSettings.browser --update
```
From the `packages/desktop-client` directory:
```sh
# Run all browser tests
yarn test:browser:docker
# Run a specific browser test file
yarn test:browser:docker AuthSettings.browser
# Run with update flag
yarn test:browser:docker AuthSettings.browser --update
```
### Why Docker?
Running browser tests locally will produce inconsistent screenshots due to:
- System-specific font rendering
- Different DPI/display scaling
- OS-specific rendering differences
- Font availability variations
Docker ensures all tests run in the same standardized environment (`mcr.microsoft.com/playwright:v1.56.0-jammy`), producing consistent, reproducible screenshots.

View File

@@ -47,6 +47,7 @@
"@use-gesture/react": "^10.3.1",
"@vitejs/plugin-basic-ssl": "^2.1.3",
"@vitejs/plugin-react": "^5.1.2",
"@vitest/browser-playwright": "^4.0.17",
"auto-text-size": "^0.2.3",
"babel-plugin-react-compiler": "^1.0.0",
"cmdk": "^1.1.1",
@@ -93,8 +94,9 @@
"uuid": "^13.0.0",
"vite": "^7.3.1",
"vite-plugin-pwa": "^1.2.0",
"vite-tsconfig-paths": "^5.1.4",
"vite-tsconfig-paths": "^6.0.4",
"vitest": "^4.0.16",
"vitest-browser-react": "^2.0.2",
"xml2js": "^0.6.2"
}
}

View File

@@ -0,0 +1,160 @@
/**
* Browser Visual Regression Tests
*
* IMPORTANT: These tests generate screenshots that vary by environment (fonts, rendering, etc.).
* For consistent screenshot quality, always run these tests in Docker:
*
* From root: yarn test:browser:docker AuthSettings.browser
* Or: cd packages/desktop-client && yarn test:browser:docker AuthSettings.browser
*
* Running locally will produce inconsistent screenshots due to system-specific rendering differences.
*/
import { expect, test, vi, beforeEach, describe } from 'vitest';
import { render } from 'vitest-browser-react';
import { page } from 'vitest/browser';
import { AuthSettings } from './AuthSettings';
import {
useMultiuserEnabled,
useLoginMethod,
} from '@desktop-client/components/ServerContext';
import { useSyncServerStatus } from '@desktop-client/hooks/useSyncServerStatus';
import { BrowserTestProvider } from '@desktop-client/redux/mock';
// Mock the hooks that AuthSettings depends on
vi.mock('@desktop-client/hooks/useSyncServerStatus', () => ({
useSyncServerStatus: vi.fn(),
}));
vi.mock('@desktop-client/components/ServerContext', () => ({
useMultiuserEnabled: vi.fn(),
useLoginMethod: vi.fn(),
}));
describe('AuthSettings Visual Regression', () => {
beforeEach(() => {
vi.clearAllMocks();
});
test.skip('does not render OpenID block when server status is no-server', async () => {
vi.mocked(useSyncServerStatus).mockReturnValue('no-server');
vi.mocked(useMultiuserEnabled).mockReturnValue(false);
vi.mocked(useLoginMethod).mockReturnValue('password');
await render(<AuthSettings />, { wrapper: BrowserTestProvider });
// Component should return null, so trying to get "authentication method" text should fail
// This test verifies the component doesn't render at all
// Since the component returns null, nothing is rendered
try {
const authMethodText = page.getByText(/authentication method/i);
await expect.element(authMethodText).toBeVisible();
// If we get here, the element exists, which means the test should fail
expect.fail('Expected component to return null, but it rendered');
} catch (error) {
// Element not found is expected, so the test passes
expect(error).toBeDefined();
}
});
// TODO: render permutations
test.only('renders disabled OpenID block with warning when server is offline', async () => {
vi.mocked(useSyncServerStatus).mockReturnValue('offline');
vi.mocked(useMultiuserEnabled).mockReturnValue(false);
vi.mocked(useLoginMethod).mockReturnValue('password');
await render(
<div
data-testid="auth-settings-wrapper"
style={{
minWidth: '1200px',
width: '1200px',
padding: '20px',
boxSizing: 'border-box',
// Ensure the element can expand to full width
display: 'block',
overflow: 'visible',
// Force the element to be exactly 1200px
maxWidth: '1200px',
position: 'relative',
}}
>
<AuthSettings />
</div>,
{
wrapper: BrowserTestProvider,
},
);
// Wait for the element to be fully rendered and visible
const wrapper = page.getByTestId('auth-settings-wrapper');
await expect(wrapper).toBeVisible();
// Wait for rendering to complete
await new Promise(resolve => setTimeout(resolve, 200));
// Use toMatchScreenshot - scale: 'device' is set globally in vite.config.mts
// This should use device pixels (respecting deviceScaleFactor: 3) instead of CSS pixels
// With deviceScaleFactor: 3, screenshots should be 3x the CSS size
//
// NOTE: If screenshots are still low-res, this is likely a bug in vitest-browser
// where toMatchScreenshot doesn't properly respect deviceScaleFactor even with scale: 'device'
await expect(wrapper).toMatchScreenshot();
});
test('renders enabled OpenID block when server is online with password login', async () => {
vi.mocked(useSyncServerStatus).mockReturnValue('online');
vi.mocked(useMultiuserEnabled).mockReturnValue(false);
vi.mocked(useLoginMethod).mockReturnValue('password');
await render(
<div
data-testid="auth-settings-wrapper"
style={{
minWidth: '1200px',
width: '1200px',
padding: '20px',
boxSizing: 'border-box',
}}
>
<AuthSettings />
</div>,
{
wrapper: BrowserTestProvider,
},
);
const wrapper = page.getByTestId('auth-settings-wrapper');
// scale: 'device' is set globally in vite.config.mts
await expect(wrapper).toMatchScreenshot();
});
test('renders OpenID enabled state when server is online with OpenID login', async () => {
vi.mocked(useSyncServerStatus).mockReturnValue('online');
vi.mocked(useMultiuserEnabled).mockReturnValue(false);
vi.mocked(useLoginMethod).mockReturnValue('openid');
await render(
<div
data-testid="auth-settings-wrapper"
style={{
minWidth: '1200px',
width: '1200px',
padding: '20px',
boxSizing: 'border-box',
}}
>
<AuthSettings />
</div>,
{
wrapper: BrowserTestProvider,
},
);
const wrapper = page.getByTestId('auth-settings-wrapper');
// scale: 'device' is set globally in vite.config.mts
await expect(wrapper).toMatchScreenshot();
});
});

View File

@@ -0,0 +1,178 @@
import { render, screen } from '@testing-library/react';
import { vi } from 'vitest';
import { AuthSettings } from './AuthSettings';
import {
useMultiuserEnabled,
useLoginMethod,
} from '@desktop-client/components/ServerContext';
import { useSyncServerStatus } from '@desktop-client/hooks/useSyncServerStatus';
import { TestProvider } from '@desktop-client/redux/mock';
vi.mock('@desktop-client/hooks/useSyncServerStatus', () => ({
useSyncServerStatus: vi.fn(),
}));
vi.mock('@desktop-client/components/ServerContext', () => ({
useMultiuserEnabled: vi.fn(),
useLoginMethod: vi.fn(),
}));
vi.mock('@desktop-client/redux', () => ({
useDispatch: vi.fn(() => vi.fn()),
}));
describe('AuthSettings', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('does not render when server status is no-server', () => {
vi.mocked(useSyncServerStatus).mockReturnValue('no-server');
vi.mocked(useMultiuserEnabled).mockReturnValue(false);
vi.mocked(useLoginMethod).mockReturnValue('password');
const { container } = render(
<TestProvider>
<AuthSettings />
</TestProvider>,
);
expect(container.firstChild).toBeNull();
});
describe('when server is offline', () => {
beforeEach(() => {
vi.mocked(useSyncServerStatus).mockReturnValue('offline');
});
it('disables buttons and shows warning when login method is password', () => {
vi.mocked(useMultiuserEnabled).mockReturnValue(false);
vi.mocked(useLoginMethod).mockReturnValue('password');
render(
<TestProvider>
<AuthSettings />
</TestProvider>,
);
const startUsingButton = screen.getByRole('button', {
name: /start using openid/i,
});
expect(startUsingButton).toBeDisabled();
const warningText = screen.getByText(
/server is offline\. openid settings are unavailable\./i,
);
expect(warningText).toBeInTheDocument();
});
it('disables buttons and shows warning when login method is openid', () => {
vi.mocked(useMultiuserEnabled).mockReturnValue(false);
vi.mocked(useLoginMethod).mockReturnValue('openid');
render(
<TestProvider>
<AuthSettings />
</TestProvider>,
);
const disableButton = screen.getByRole('button', {
name: /disable openid/i,
});
expect(disableButton).toBeDisabled();
const warningText = screen.getByText(
/server is offline\. openid settings are unavailable\./i,
);
expect(warningText).toBeInTheDocument();
});
it('hides the label hint when offline', () => {
vi.mocked(useSyncServerStatus).mockReturnValue('offline');
vi.mocked(useMultiuserEnabled).mockReturnValue(false);
vi.mocked(useLoginMethod).mockReturnValue('password');
render(
<TestProvider>
<AuthSettings />
</TestProvider>,
);
const labelHint = screen.queryByText(
/openid is required to enable multi-user mode\./i,
);
expect(labelHint).not.toBeInTheDocument();
});
});
describe('when server is online', () => {
beforeEach(() => {
vi.mocked(useSyncServerStatus).mockReturnValue('online');
});
it('renders normally with password login method', () => {
vi.mocked(useMultiuserEnabled).mockReturnValue(false);
vi.mocked(useLoginMethod).mockReturnValue('password');
render(
<TestProvider>
<AuthSettings />
</TestProvider>,
);
const startUsingButton = screen.getByRole('button', {
name: /start using openid/i,
});
expect(startUsingButton).not.toBeDisabled();
const warningText = screen.queryByText(
/server is offline\. openid settings are unavailable\./i,
);
expect(warningText).not.toBeInTheDocument();
const labelHint = screen.getByText(
/openid is required to enable multi-user mode\./i,
);
expect(labelHint).toBeInTheDocument();
});
it('renders normally with openid login method', () => {
vi.mocked(useMultiuserEnabled).mockReturnValue(false);
vi.mocked(useLoginMethod).mockReturnValue('openid');
render(
<TestProvider>
<AuthSettings />
</TestProvider>,
);
const disableButton = screen.getByRole('button', {
name: /disable openid/i,
});
expect(disableButton).not.toBeDisabled();
const warningText = screen.queryByText(
/server is offline\. openid settings are unavailable\./i,
);
expect(warningText).not.toBeInTheDocument();
});
it('shows multi-user warning when multiuser is enabled', () => {
vi.mocked(useMultiuserEnabled).mockReturnValue(true);
vi.mocked(useLoginMethod).mockReturnValue('openid');
render(
<TestProvider>
<AuthSettings />
</TestProvider>,
);
const warningText = screen.getByText(
/disabling openid will deactivate multi-user mode\./i,
);
expect(warningText).toBeInTheDocument();
});
});
});

View File

@@ -5,6 +5,7 @@ import { Button } from '@actual-app/components/button';
import { Label } from '@actual-app/components/label';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import { Setting } from './UI';
@@ -12,6 +13,7 @@ import {
useMultiuserEnabled,
useLoginMethod,
} from '@desktop-client/components/ServerContext';
import { useSyncServerStatus } from '@desktop-client/hooks/useSyncServerStatus';
import { pushModal } from '@desktop-client/modals/modalsSlice';
import { useDispatch } from '@desktop-client/redux';
@@ -21,6 +23,14 @@ export function AuthSettings() {
const multiuserEnabled = useMultiuserEnabled();
const loginMethod = useLoginMethod();
const dispatch = useDispatch();
const serverStatus = useSyncServerStatus();
// Hide the OpenID block entirely when no server is configured
if (serverStatus === 'no-server') {
return null;
}
const isOffline = serverStatus === 'offline';
return (
<Setting
@@ -32,6 +42,15 @@ export function AuthSettings() {
{loginMethod === 'openid' ? t('enabled') : t('disabled')}
</label>
</label>
{isOffline && (
<View>
<Text style={{ paddingTop: 5, color: theme.errorText }}>
<Trans>
Server is offline. OpenID settings are unavailable.
</Trans>
</Text>
</View>
)}
{loginMethod === 'password' && (
<>
<Button
@@ -40,6 +59,7 @@ export function AuthSettings() {
marginTop: '10px',
}}
variant="normal"
isDisabled={isOffline}
onPress={() =>
dispatch(
pushModal({
@@ -53,10 +73,12 @@ export function AuthSettings() {
>
<Trans>Start using OpenID</Trans>
</Button>
<Label
style={{ paddingTop: 5 }}
title={t('OpenID is required to enable multi-user mode.')}
/>
{!isOffline && (
<Label
style={{ paddingTop: 5 }}
title={t('OpenID is required to enable multi-user mode.')}
/>
)}
</>
)}
{loginMethod !== 'password' && (
@@ -66,6 +88,7 @@ export function AuthSettings() {
marginTop: '10px',
}}
variant="normal"
isDisabled={isOffline}
onPress={() =>
dispatch(
pushModal({

View File

@@ -37,6 +37,7 @@ import {
name as prefsSliceName,
reducer as prefsSliceReducer,
} from '@desktop-client/prefs/prefsSlice';
import { ThemeStyle } from '@desktop-client/style';
import {
name as tagsSliceName,
reducer as tagsSliceReducer,
@@ -77,3 +78,12 @@ export function resetMockStore() {
export function TestProvider({ children }: { children: ReactNode }) {
return <Provider store={mockStore}>{children}</Provider>;
}
export function BrowserTestProvider({ children }: { children: ReactNode }) {
return (
<TestProvider>
<ThemeStyle />
{children}
</TestProvider>
);
}

View File

@@ -2,6 +2,7 @@ import * as path from 'path';
import inject from '@rollup/plugin-inject';
import basicSsl from '@vitejs/plugin-basic-ssl';
import { playwright } from '@vitest/browser-playwright';
import react from '@vitejs/plugin-react';
import type { PreRenderedAsset } from 'rollup';
import { visualizer } from 'rollup-plugin-visualizer';
@@ -210,17 +211,65 @@ export default defineConfig(async ({ mode }) => {
visualizer({ template: 'raw-data' }),
!!env.HTTPS && basicSsl(),
],
// @ts-expect-error - test is not a valid property of UserConfig
test: {
include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
environment: 'jsdom',
globals: true,
setupFiles: './src/setupTests.js',
testTimeout: 10000,
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
// print only console.error
return type === 'stderr';
},
maxWorkers: 2,
projects: [
{
extends: true,
test: {
include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
exclude: ['src/**/*.browser.test.{ts,tsx}'],
name: 'jsdom',
environment: 'jsdom',
globals: true,
setupFiles: './src/setupTests.js',
},
},
{
extends: true,
test: {
include: ['src/**/*.browser.test.{ts,tsx}'],
name: 'browser',
globals: true,
browser: {
enabled: true,
provider: playwright(),
instances: [
{
browser: 'chromium',
headless: true,
contextOptions: {
// Large viewport to accommodate 1200px wide elements
viewport: { width: 1920, height: 1080 },
// deviceScaleFactor: 3 means screenshots will be 3x the CSS size
// So a 1200px element will produce a 3600px wide screenshot
deviceScaleFactor: 3,
},
},
],
},
expect: {
toMatchScreenshot: {
comparatorOptions: {
threshold: 0.2,
},
// Set default screenshot scale to 'device' to respect deviceScaleFactor: 3
// This should make screenshots use device pixels instead of CSS pixels
screenshotOptions: {
scale: 'device', // Use device pixels - respects deviceScaleFactor: 3
},
},
},
},
},
],
},
};
});

360
yarn.lock
View File

@@ -163,6 +163,7 @@ __metadata:
"@use-gesture/react": "npm:^10.3.1"
"@vitejs/plugin-basic-ssl": "npm:^2.1.3"
"@vitejs/plugin-react": "npm:^5.1.2"
"@vitest/browser-playwright": "npm:^4.0.17"
auto-text-size: "npm:^0.2.3"
babel-plugin-react-compiler: "npm:^1.0.0"
cmdk: "npm:^1.1.1"
@@ -209,8 +210,9 @@ __metadata:
uuid: "npm:^13.0.0"
vite: "npm:^7.3.1"
vite-plugin-pwa: "npm:^1.2.0"
vite-tsconfig-paths: "npm:^5.1.4"
vite-tsconfig-paths: "npm:^6.0.4"
vitest: "npm:^4.0.16"
vitest-browser-react: "npm:^2.0.2"
xml2js: "npm:^0.6.2"
languageName: unknown
linkType: soft
@@ -4043,7 +4045,7 @@ __metadata:
languageName: node
linkType: hard
"@eslint-community/eslint-utils@npm:^4.7.0":
"@eslint-community/eslint-utils@npm:^4.7.0, @eslint-community/eslint-utils@npm:^4.8.0":
version: 4.9.0
resolution: "@eslint-community/eslint-utils@npm:4.9.0"
dependencies:
@@ -4054,21 +4056,10 @@ __metadata:
languageName: node
linkType: hard
"@eslint-community/eslint-utils@npm:^4.8.0":
version: 4.9.1
resolution: "@eslint-community/eslint-utils@npm:4.9.1"
dependencies:
eslint-visitor-keys: "npm:^3.4.3"
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
checksum: 10/863b5467868551c9ae34d03eefe634633d08f623fc7b19d860f8f26eb6f303c1a5934253124163bee96181e45ed22bf27473dccc295937c3078493a4a8c9eddd
languageName: node
linkType: hard
"@eslint-community/regexpp@npm:^4.12.1":
version: 4.12.2
resolution: "@eslint-community/regexpp@npm:4.12.2"
checksum: 10/049b280fddf71dd325514e0a520024969431dc3a8b02fa77476e6820e9122f28ab4c9168c11821f91a27982d2453bcd7a66193356ea84e84fb7c8d793be1ba0c
version: 4.12.1
resolution: "@eslint-community/regexpp@npm:4.12.1"
checksum: 10/c08f1dd7dd18fbb60bdd0d85820656d1374dd898af9be7f82cb00451313402a22d5e30569c150315b4385907cdbca78c22389b2a72ab78883b3173be317620cc
languageName: node
linkType: hard
@@ -4102,8 +4093,8 @@ __metadata:
linkType: hard
"@eslint/eslintrc@npm:^3.3.1":
version: 3.3.3
resolution: "@eslint/eslintrc@npm:3.3.3"
version: 3.3.1
resolution: "@eslint/eslintrc@npm:3.3.1"
dependencies:
ajv: "npm:^6.12.4"
debug: "npm:^4.3.2"
@@ -4111,10 +4102,10 @@ __metadata:
globals: "npm:^14.0.0"
ignore: "npm:^5.2.0"
import-fresh: "npm:^3.2.1"
js-yaml: "npm:^4.1.1"
js-yaml: "npm:^4.1.0"
minimatch: "npm:^3.1.2"
strip-json-comments: "npm:^3.1.1"
checksum: 10/b586a364ff15ce1b68993aefc051ca330b1fece15fb5baf4a708d00113f9a14895cffd84a5f24c5a97bd4b4321130ab2314f90aa462a250f6b859c2da2cba1f3
checksum: 10/cc240addbab3c5fceaa65b2c8d5d4fd77ddbbf472c2f74f0270b9d33263dc9116840b6099c46b64c9680301146250439b044ed79278a1bcc557da412a4e3c1bb
languageName: node
linkType: hard
@@ -9578,19 +9569,6 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/project-service@npm:8.46.4":
version: 8.46.4
resolution: "@typescript-eslint/project-service@npm:8.46.4"
dependencies:
"@typescript-eslint/tsconfig-utils": "npm:^8.46.4"
"@typescript-eslint/types": "npm:^8.46.4"
debug: "npm:^4.3.4"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/f145da5f0c063833f48d36f2c3a19a37e2fb77156f0cc7046ee15f2e59418309b95628c8e7216e4429fac9f1257fab945c5d3f5abfd8f924223d36125c633d32
languageName: node
linkType: hard
"@typescript-eslint/project-service@npm:8.50.0":
version: 8.50.0
resolution: "@typescript-eslint/project-service@npm:8.50.0"
@@ -9604,16 +9582,6 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:8.46.4":
version: 8.46.4
resolution: "@typescript-eslint/scope-manager@npm:8.46.4"
dependencies:
"@typescript-eslint/types": "npm:8.46.4"
"@typescript-eslint/visitor-keys": "npm:8.46.4"
checksum: 10/1439ffc1458281282c1ae3aabbe89140ce15c796d4f1c59f0de38e8536803e10143fe322a7e1cb56fe41da9e4617898d70923b71621b47cff4472aa5dae88d7e
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:8.50.0":
version: 8.50.0
resolution: "@typescript-eslint/scope-manager@npm:8.50.0"
@@ -9624,15 +9592,6 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/tsconfig-utils@npm:8.46.4, @typescript-eslint/tsconfig-utils@npm:^8.46.4":
version: 8.46.4
resolution: "@typescript-eslint/tsconfig-utils@npm:8.46.4"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/eda25b1daee6abf51ee2dd5fc1dc1a5160a14301c0e7bed301ec5eb0f7b45418d509c035361f88a37f4af9771d7334f1dcb9bc7f7a38f07b09e85d4d9d92767f
languageName: node
linkType: hard
"@typescript-eslint/tsconfig-utils@npm:8.50.0, @typescript-eslint/tsconfig-utils@npm:^8.50.0":
version: 8.50.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.50.0"
@@ -9642,13 +9601,6 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.46.4, @typescript-eslint/types@npm:^8.46.4":
version: 8.46.4
resolution: "@typescript-eslint/types@npm:8.46.4"
checksum: 10/dd71692722254308f7954ade97800c141ec4a2bbdeef334df4ef9a5ee00db4597db4c3d0783607fc61c22238c9c534803a5421fe0856033a635e13fbe99b3cf0
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.50.0, @typescript-eslint/types@npm:^8.38.0, @typescript-eslint/types@npm:^8.50.0":
version: 8.50.0
resolution: "@typescript-eslint/types@npm:8.50.0"
@@ -9656,26 +9608,6 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:8.46.4":
version: 8.46.4
resolution: "@typescript-eslint/typescript-estree@npm:8.46.4"
dependencies:
"@typescript-eslint/project-service": "npm:8.46.4"
"@typescript-eslint/tsconfig-utils": "npm:8.46.4"
"@typescript-eslint/types": "npm:8.46.4"
"@typescript-eslint/visitor-keys": "npm:8.46.4"
debug: "npm:^4.3.4"
fast-glob: "npm:^3.3.2"
is-glob: "npm:^4.0.3"
minimatch: "npm:^9.0.4"
semver: "npm:^7.6.0"
ts-api-utils: "npm:^2.1.0"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/2a932bdd7ac260e2b7290c952241bf06b2ddbeb3cf636bc624a64a9cfb046619620172a1967f30dbde6ac5f4fbdcfec66e1349af46313da86e01b5575dfebe2e
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:8.50.0":
version: 8.50.0
resolution: "@typescript-eslint/typescript-estree@npm:8.50.0"
@@ -9695,7 +9627,7 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:^8.38.0":
"@typescript-eslint/utils@npm:^8.38.0, @typescript-eslint/utils@npm:^8.46.2":
version: 8.50.0
resolution: "@typescript-eslint/utils@npm:8.50.0"
dependencies:
@@ -9710,31 +9642,6 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:^8.46.2":
version: 8.46.4
resolution: "@typescript-eslint/utils@npm:8.46.4"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.7.0"
"@typescript-eslint/scope-manager": "npm:8.46.4"
"@typescript-eslint/types": "npm:8.46.4"
"@typescript-eslint/typescript-estree": "npm:8.46.4"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/8e11abb2e44b6e62ccf8fd9b96808cb58e68788564fa999f15b61c0ec929209ced7f92a57ffbfcaec80f926aa14dafcee756755b724ae543b4cbd84b0ffb890d
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:8.46.4":
version: 8.46.4
resolution: "@typescript-eslint/visitor-keys@npm:8.46.4"
dependencies:
"@typescript-eslint/types": "npm:8.46.4"
eslint-visitor-keys: "npm:^4.2.1"
checksum: 10/bcf479fa5c59857cf7aa7b90d9c00e23f7303473b94a401cc3b64776ebb66978b5342459a1672581dcf1861fa5961bb59c901fe766c28b6bc3f93e60bfc34dae
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:8.50.0":
version: 8.50.0
resolution: "@typescript-eslint/visitor-keys@npm:8.50.0"
@@ -9847,6 +9754,41 @@ __metadata:
languageName: node
linkType: hard
"@vitest/browser-playwright@npm:^4.0.17":
version: 4.0.17
resolution: "@vitest/browser-playwright@npm:4.0.17"
dependencies:
"@vitest/browser": "npm:4.0.17"
"@vitest/mocker": "npm:4.0.17"
tinyrainbow: "npm:^3.0.3"
peerDependencies:
playwright: "*"
vitest: 4.0.17
peerDependenciesMeta:
playwright:
optional: false
checksum: 10/ba887cb43d72178a133b51a68a74de45ef4a6d940a924444d777224ac67abf5703445430216ca6c8c6c267f7064113591a4d1f4f4b3ddd18c9f539cf4aaccd3c
languageName: node
linkType: hard
"@vitest/browser@npm:4.0.17":
version: 4.0.17
resolution: "@vitest/browser@npm:4.0.17"
dependencies:
"@vitest/mocker": "npm:4.0.17"
"@vitest/utils": "npm:4.0.17"
magic-string: "npm:^0.30.21"
pixelmatch: "npm:7.1.0"
pngjs: "npm:^7.0.0"
sirv: "npm:^3.0.2"
tinyrainbow: "npm:^3.0.3"
ws: "npm:^8.18.3"
peerDependencies:
vitest: 4.0.17
checksum: 10/d89b7773a385dbf4594eafca288a63579f79ce6555978bcc11567b9f47376ad4393010915f133810b653fb118026775247b90ba59b00f245bb09a9356a474a6b
languageName: node
linkType: hard
"@vitest/expect@npm:4.0.17":
version: 4.0.17
resolution: "@vitest/expect@npm:4.0.17"
@@ -12217,14 +12159,7 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:^14.0.0":
version: 14.0.1
resolution: "commander@npm:14.0.1"
checksum: 10/783115e9403caeca29c0fcbd4e0358f70c67760e4e4933f3453fcdd5ddba2ec44173c8da5213d7ce5e404f51c7e71203a42c548164dbe27b668b32a8981577f1
languageName: node
linkType: hard
"commander@npm:^14.0.2":
"commander@npm:^14.0.0, commander@npm:^14.0.2":
version: 14.0.2
resolution: "commander@npm:14.0.2"
checksum: 10/2d202db5e5f9bb770112a3c1579b893d17ac6f6d932183077308bdd96d0f87f0bbe6a68b5b9ed2cf3b2514be6bb7de637480703c0e2db9741ee1b383237deb26
@@ -14987,11 +14922,11 @@ __metadata:
linkType: hard
"esquery@npm:^1.5.0":
version: 1.7.0
resolution: "esquery@npm:1.7.0"
version: 1.6.0
resolution: "esquery@npm:1.6.0"
dependencies:
estraverse: "npm:^5.1.0"
checksum: 10/4afaf3089367e1f5885caa116ef386dffd8bfd64da21fd3d0e56e938d2667cfb2e5400ab4a825aa70e799bb3741e5b5d63c0b94d86e2d4cf3095c9e64b2f5a15
checksum: 10/c587fb8ec9ed83f2b1bc97cf2f6854cc30bf784a79d62ba08c6e358bf22280d69aee12827521cf38e69ae9761d23fb7fde593ce315610f85655c139d99b05e5a
languageName: node
linkType: hard
@@ -15439,7 +15374,7 @@ __metadata:
languageName: node
linkType: hard
"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0, fast-glob@npm:^3.3.2":
"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0":
version: 3.3.3
resolution: "fast-glob@npm:3.3.3"
dependencies:
@@ -17005,7 +16940,7 @@ __metadata:
languageName: node
linkType: hard
"http-errors@npm:2.0.0, http-errors@npm:^2.0.0":
"http-errors@npm:2.0.0":
version: 2.0.0
resolution: "http-errors@npm:2.0.0"
dependencies:
@@ -17018,19 +16953,7 @@ __metadata:
languageName: node
linkType: hard
"http-errors@npm:~1.6.2":
version: 1.6.3
resolution: "http-errors@npm:1.6.3"
dependencies:
depd: "npm:~1.1.2"
inherits: "npm:2.0.3"
setprototypeof: "npm:1.1.0"
statuses: "npm:>= 1.4.0 < 2"
checksum: 10/e48732657ea0b4a09853d2696a584fa59fa2a8c1ba692af7af3137b5491a997d7f9723f824e7e08eb6a87098532c09ce066966ddf0f9f3dd30905e52301acadb
languageName: node
linkType: hard
"http-errors@npm:~2.0.1":
"http-errors@npm:^2.0.0, http-errors@npm:~2.0.1":
version: 2.0.1
resolution: "http-errors@npm:2.0.1"
dependencies:
@@ -17043,6 +16966,18 @@ __metadata:
languageName: node
linkType: hard
"http-errors@npm:~1.6.2":
version: 1.6.3
resolution: "http-errors@npm:1.6.3"
dependencies:
depd: "npm:~1.1.2"
inherits: "npm:2.0.3"
setprototypeof: "npm:1.1.0"
statuses: "npm:>= 1.4.0 < 2"
checksum: 10/e48732657ea0b4a09853d2696a584fa59fa2a8c1ba692af7af3137b5491a997d7f9723f824e7e08eb6a87098532c09ce066966ddf0f9f3dd30905e52301acadb
languageName: node
linkType: hard
"http-parser-js@npm:>=0.5.1":
version: 0.5.10
resolution: "http-parser-js@npm:0.5.10"
@@ -18371,17 +18306,6 @@ __metadata:
languageName: node
linkType: hard
"js-yaml@npm:^4.1.1":
version: 4.1.1
resolution: "js-yaml@npm:4.1.1"
dependencies:
argparse: "npm:^2.0.1"
bin:
js-yaml: bin/js-yaml.js
checksum: 10/a52d0519f0f4ef5b4adc1cde466cb54c50d56e2b4a983b9d5c9c0f2f99462047007a6274d7e95617a21d3c91fde3ee6115536ed70991cd645ba8521058b78f77
languageName: node
linkType: hard
"jsdom@npm:^27.4.0":
version: 27.4.0
resolution: "jsdom@npm:27.4.0"
@@ -21911,6 +21835,17 @@ __metadata:
languageName: node
linkType: hard
"pixelmatch@npm:7.1.0":
version: 7.1.0
resolution: "pixelmatch@npm:7.1.0"
dependencies:
pngjs: "npm:^7.0.0"
bin:
pixelmatch: bin/pixelmatch
checksum: 10/57a122196318ea8ce74e8759b1b7b94b9f9627b495cd79e50a49d470dc23b6c679e89c38660d0f7e8f959eac3b279c55b728e52d02c276dc51505f06eaba1141
languageName: node
linkType: hard
"pkg-dir@npm:^5.0.0":
version: 5.0.0
resolution: "pkg-dir@npm:5.0.0"
@@ -22017,6 +21952,13 @@ __metadata:
languageName: unknown
linkType: soft
"pngjs@npm:^7.0.0":
version: 7.0.0
resolution: "pngjs@npm:7.0.0"
checksum: 10/e843ebbb0df092ee0f3a3e7dbd91ff87a239a4e4c4198fff202916bfb33b67622f4b83b3c29f3ccae94fcb97180c289df06068624554f61686fe6b9a4811f7db
languageName: node
linkType: hard
"points-on-curve@npm:0.2.0, points-on-curve@npm:^0.2.0":
version: 0.2.0
resolution: "points-on-curve@npm:0.2.0"
@@ -25398,6 +25340,17 @@ __metadata:
languageName: node
linkType: hard
"sirv@npm:^3.0.2":
version: 3.0.2
resolution: "sirv@npm:3.0.2"
dependencies:
"@polka/url": "npm:^1.0.0-next.24"
mrmime: "npm:^2.0.0"
totalist: "npm:^3.0.0"
checksum: 10/259617f4ab57664be6d963f5b27b38a6351d3e91ce70d6726985d087b40efd595fcf7f72ae010babf5e0acb63bcb3e3d6db8de34604da1011be6e28ee32aa15d
languageName: node
linkType: hard
"sisteransi@npm:^1.0.5":
version: 1.0.5
resolution: "sisteransi@npm:1.0.5"
@@ -27723,19 +27676,75 @@ __metadata:
languageName: node
linkType: hard
"vite-tsconfig-paths@npm:^5.1.4":
version: 5.1.4
resolution: "vite-tsconfig-paths@npm:5.1.4"
"vite-tsconfig-paths@npm:^6.0.4":
version: 6.0.4
resolution: "vite-tsconfig-paths@npm:6.0.4"
dependencies:
debug: "npm:^4.1.1"
globrex: "npm:^0.1.2"
tsconfck: "npm:^3.0.3"
vite: "npm:*"
peerDependencies:
vite: "*"
peerDependenciesMeta:
vite:
optional: true
checksum: 10/b409dbd17829f560021a71dba3e473b9c06dcf5fdc9d630b72c1f787145ec478b38caff1be04868971ac8bdcbf0f5af45eeece23dbc9c59c54b901f867740ae0
checksum: 10/85f871cd5e321f2865972559b01c518664e6e34f9039b630dd77c2f379f8fdc386e15f7237aa5c108d813030c6e9bc8edfbf61687df7684803111a2495edadac
languageName: node
linkType: hard
"vite@npm:*, vite@npm:^7.3.1":
version: 7.3.1
resolution: "vite@npm:7.3.1"
dependencies:
esbuild: "npm:^0.27.0"
fdir: "npm:^6.5.0"
fsevents: "npm:~2.3.3"
picomatch: "npm:^4.0.3"
postcss: "npm:^8.5.6"
rollup: "npm:^4.43.0"
tinyglobby: "npm:^0.2.15"
peerDependencies:
"@types/node": ^20.19.0 || >=22.12.0
jiti: ">=1.21.0"
less: ^4.0.0
lightningcss: ^1.21.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: ">=0.54.8"
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
dependenciesMeta:
fsevents:
optional: true
peerDependenciesMeta:
"@types/node":
optional: true
jiti:
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
bin:
vite: bin/vite.js
checksum: 10/62e48ffa4283b688f0049005405a004447ad38ffc99a0efea4c3aa9b7eed739f7402b43f00668c0ee5a895b684dc953d62f0722d8a92c5b2f6c95f051bceb208
languageName: node
linkType: hard
@@ -27794,58 +27803,21 @@ __metadata:
languageName: node
linkType: hard
"vite@npm:^7.3.1":
version: 7.3.1
resolution: "vite@npm:7.3.1"
dependencies:
esbuild: "npm:^0.27.0"
fdir: "npm:^6.5.0"
fsevents: "npm:~2.3.3"
picomatch: "npm:^4.0.3"
postcss: "npm:^8.5.6"
rollup: "npm:^4.43.0"
tinyglobby: "npm:^0.2.15"
"vitest-browser-react@npm:^2.0.2":
version: 2.0.2
resolution: "vitest-browser-react@npm:2.0.2"
peerDependencies:
"@types/node": ^20.19.0 || >=22.12.0
jiti: ">=1.21.0"
less: ^4.0.0
lightningcss: ^1.21.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: ">=0.54.8"
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
dependenciesMeta:
fsevents:
optional: true
"@types/react": ^18.0.0 || ^19.0.0
"@types/react-dom": ^18.0.0 || ^19.0.0
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
vitest: ^4.0.0
peerDependenciesMeta:
"@types/node":
"@types/react":
optional: true
jiti:
"@types/react-dom":
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
bin:
vite: bin/vite.js
checksum: 10/62e48ffa4283b688f0049005405a004447ad38ffc99a0efea4c3aa9b7eed739f7402b43f00668c0ee5a895b684dc953d62f0722d8a92c5b2f6c95f051bceb208
checksum: 10/1a026341d7b6300261e900eb34765128174876cdf71e58cd7ff244190e7c390ee60e8f6c5ceaec2bb79c8fcc3355d13274151a7d23d89d8001e5124311e4d0ec
languageName: node
linkType: hard