mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
test(app): remove useless frontend tests (#2819)
This commit is contained in:
1 parent
ed9528834d
commit
deac653a9e
341 files changed
-62758
No files matched your search
-50
@@ -1,50 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
cssColorStringToHex,
|
||||
cssColorStringToNumber,
|
||||
numberToHex,
|
||||
updateCssForVariable,
|
||||
} from '@app/features/user/components/modals/tabs/appearance_tab/theme/ThemeUtils';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
describe('updateCssForVariable', () => {
|
||||
it('adds a token override when the CSS is empty', () => {
|
||||
expect(updateCssForVariable('', '--background-primary', '#ff00ff')).toBe(
|
||||
':root { --background-primary: #ff00ff; }\n',
|
||||
);
|
||||
});
|
||||
it('updates an existing token override', () => {
|
||||
expect(updateCssForVariable(':root { --background-primary: #ffffff; }\n', '--background-primary', '#111111')).toBe(
|
||||
':root { --background-primary: #111111; }\n',
|
||||
);
|
||||
});
|
||||
it('removes the empty root block after the last token override is cleared', () => {
|
||||
expect(updateCssForVariable(':root { --background-primary: #ffffff; }\n', '--background-primary', null)).toBe('');
|
||||
});
|
||||
it('keeps unrelated CSS when clearing a token override', () => {
|
||||
expect(
|
||||
updateCssForVariable(
|
||||
'body { color: red; }\n:root { --background-primary: #ffffff; }\n',
|
||||
'--background-primary',
|
||||
null,
|
||||
),
|
||||
).toBe('body { color: red; }');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cssColorStringToNumber', () => {
|
||||
it('parses black as a real color', () => {
|
||||
expect(numberToHex(cssColorStringToNumber('#000000') ?? -1)).toBe('#000000');
|
||||
expect(numberToHex(cssColorStringToNumber('rgb(0, 0, 0)') ?? -1)).toBe('#000000');
|
||||
expect(numberToHex(cssColorStringToNumber('#000000 !important') ?? -1)).toBe('#000000');
|
||||
});
|
||||
it('does not turn invalid CSS values into black', () => {
|
||||
expect(cssColorStringToNumber('1px solid hsl(0, 0%, 0%)')).toBeNull();
|
||||
expect(cssColorStringToNumber('not-a-color')).toBeNull();
|
||||
});
|
||||
it('normalizes equivalent CSS colors to the same hex value', () => {
|
||||
expect(cssColorStringToHex('rgb(0, 0, 0)')).toBe('#000000');
|
||||
expect(cssColorStringToHex('#000000 !important')).toBe('#000000');
|
||||
});
|
||||
});
|
||||
-178
@@ -1,178 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {
|
||||
EnsureVoiceDevicesOptions,
|
||||
VoiceDeviceState,
|
||||
VoiceMediaPermissionType,
|
||||
} from '@app/features/voice/utils/VoiceDeviceManager';
|
||||
import {act, createElement, useEffect} from 'react';
|
||||
import {createRoot, type Root} from 'react-dom/client';
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
const deviceManagerMock = vi.hoisted(() => {
|
||||
const idleState = (): VoiceDeviceState => ({
|
||||
inputDevices: [],
|
||||
outputDevices: [],
|
||||
videoDevices: [],
|
||||
permissionStatus: {audio: 'idle', video: 'idle'},
|
||||
});
|
||||
const listeners = new Set<(state: VoiceDeviceState) => void>();
|
||||
const mock = {
|
||||
state: idleState(),
|
||||
permissionRequests: [] as Array<ReadonlyArray<VoiceMediaPermissionType>>,
|
||||
pending: [] as Array<(state: VoiceDeviceState) => void>,
|
||||
reset(): void {
|
||||
mock.state = idleState();
|
||||
mock.permissionRequests = [];
|
||||
mock.pending = [];
|
||||
listeners.clear();
|
||||
},
|
||||
settlePendingWith(next: VoiceDeviceState): void {
|
||||
const pending = mock.pending;
|
||||
mock.pending = [];
|
||||
mock.state = next;
|
||||
for (const listener of listeners) listener(next);
|
||||
for (const resolve of pending) resolve(next);
|
||||
},
|
||||
manager: {
|
||||
getState: (): VoiceDeviceState => mock.state,
|
||||
subscribe: (listener: (state: VoiceDeviceState) => void): (() => void) => {
|
||||
listeners.add(listener);
|
||||
listener(mock.state);
|
||||
return () => listeners.delete(listener);
|
||||
},
|
||||
ensureDevices: (options: EnsureVoiceDevicesOptions = {}): Promise<VoiceDeviceState> => {
|
||||
const requestPermissionTypes =
|
||||
options.requestPermissionTypes ?? (options.requestPermissions === true ? ['audio', 'video'] : []);
|
||||
if (requestPermissionTypes.length === 0) return Promise.resolve(mock.state);
|
||||
mock.permissionRequests.push([...requestPermissionTypes]);
|
||||
return new Promise<VoiceDeviceState>((resolve) => {
|
||||
mock.pending.push(resolve);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
return mock;
|
||||
});
|
||||
|
||||
const mediaPermissionMock = vi.hoisted(() => ({
|
||||
microphoneExplicitlyDenied: false,
|
||||
cameraExplicitlyDenied: false,
|
||||
microphonePermissionState: null as PermissionState | null,
|
||||
cameraPermissionState: null as PermissionState | null,
|
||||
isMicrophoneGranted: vi.fn(() => false),
|
||||
isCameraGranted: vi.fn(() => false),
|
||||
updateMicrophonePermissionGranted: vi.fn(),
|
||||
updateCameraPermissionGranted: vi.fn(),
|
||||
markMicrophoneExplicitlyDenied: vi.fn(),
|
||||
markCameraExplicitlyDenied: vi.fn(),
|
||||
}));
|
||||
|
||||
const handleMediaPermissionBlocked = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@app/features/voice/utils/VoiceDeviceManager', () => ({voiceDeviceManager: deviceManagerMock.manager}));
|
||||
vi.mock('@app/features/permissions/system/state/MediaPermission', () => ({default: mediaPermissionMock}));
|
||||
vi.mock('@app/features/permissions/system/commands/MacPermissionsModalCommands', () => ({
|
||||
handleMediaPermissionBlocked,
|
||||
}));
|
||||
|
||||
const {useMediaPermission} = await import('@app/features/user/components/modals/tabs/hooks/useMediaPermission');
|
||||
|
||||
(globalThis as {IS_REACT_ACT_ENVIRONMENT?: boolean}).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const MICROPHONE: MediaDeviceInfo = {
|
||||
deviceId: 'mic-1',
|
||||
groupId: 'group-1',
|
||||
kind: 'audioinput',
|
||||
label: 'Microphone',
|
||||
toJSON: () => ({}),
|
||||
};
|
||||
|
||||
const grantedState = (): VoiceDeviceState => ({
|
||||
inputDevices: [MICROPHONE],
|
||||
outputDevices: [],
|
||||
videoDevices: [],
|
||||
permissionStatus: {audio: 'granted', video: 'idle'},
|
||||
});
|
||||
|
||||
const deniedState = (): VoiceDeviceState => ({
|
||||
inputDevices: [],
|
||||
outputDevices: [],
|
||||
videoDevices: [],
|
||||
permissionStatus: {audio: 'denied', video: 'idle'},
|
||||
});
|
||||
|
||||
let host: HTMLDivElement;
|
||||
let root: Root;
|
||||
const requesters: Array<() => Promise<boolean>> = [];
|
||||
|
||||
function Consumer(): null {
|
||||
const {requestPermission} = useMediaPermission('audio');
|
||||
useEffect(() => {
|
||||
requesters.push(requestPermission);
|
||||
return () => {
|
||||
const index = requesters.indexOf(requestPermission);
|
||||
if (index !== -1) requesters.splice(index, 1);
|
||||
};
|
||||
}, [requestPermission]);
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderConsumers(count: number): void {
|
||||
act(() => {
|
||||
root.render(Array.from({length: count}, (_, index) => createElement(Consumer, {key: index})));
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
deviceManagerMock.reset();
|
||||
requesters.length = 0;
|
||||
mediaPermissionMock.microphoneExplicitlyDenied = false;
|
||||
mediaPermissionMock.microphonePermissionState = null;
|
||||
mediaPermissionMock.isMicrophoneGranted.mockReturnValue(false);
|
||||
host = document.createElement('div');
|
||||
document.body.append(host);
|
||||
root = createRoot(host);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
document.body.replaceChildren();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useMediaPermission', () => {
|
||||
it('raises a single device permission request when two consumers ask at once', async () => {
|
||||
renderConsumers(2);
|
||||
expect(requesters).toHaveLength(2);
|
||||
|
||||
const results = await act(async () => {
|
||||
const pending = Promise.all([requesters[0](), requesters[1]()]);
|
||||
await Promise.resolve();
|
||||
deviceManagerMock.settlePendingWith(grantedState());
|
||||
return pending;
|
||||
});
|
||||
|
||||
expect(deviceManagerMock.permissionRequests).toEqual([['audio']]);
|
||||
expect(results).toEqual([true, true]);
|
||||
expect(mediaPermissionMock.updateMicrophonePermissionGranted).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('reports a denied request through the shared permission state', async () => {
|
||||
renderConsumers(1);
|
||||
|
||||
const granted = await act(async () => {
|
||||
const pending = requesters[0]();
|
||||
await Promise.resolve();
|
||||
deviceManagerMock.settlePendingWith(deniedState());
|
||||
return pending;
|
||||
});
|
||||
|
||||
expect(granted).toBe(false);
|
||||
expect(mediaPermissionMock.markMicrophoneExplicitlyDenied).toHaveBeenCalledTimes(1);
|
||||
expect(handleMediaPermissionBlocked).toHaveBeenCalledWith('microphone');
|
||||
});
|
||||
});
|
||||
-189
@@ -1,189 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
assignProfileAssetUploadPatch,
|
||||
createGlobalProfileAssetRemoteState,
|
||||
createProfileAssetCustomizationSnapshot,
|
||||
createProfileAssetRemoteStateFromFlags,
|
||||
getProfileAssetUploadPatch,
|
||||
type ProfileAssetCustomizationEvent,
|
||||
type ProfileAssetCustomizationSnapshot,
|
||||
type ProfileAssetMode,
|
||||
type ProfileAssetRemoteState,
|
||||
selectProfileAssetCustomizationState,
|
||||
transitionProfileAssetCustomizationSnapshot,
|
||||
} from '@app/features/user/components/modals/tabs/my_profile_tab/ProfileAssetCustomizationStateMachine';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
const IDENTITY = 'user-1:guild-1';
|
||||
const OTHER_IDENTITY = 'user-1:guild-2';
|
||||
const DATA_URL = 'data:image/png;base64,AAA';
|
||||
|
||||
function remote(mode: ProfileAssetMode, hasCustomAsset: boolean, identityKey = IDENTITY): ProfileAssetRemoteState {
|
||||
return {identityKey, mode, hasCustomAsset};
|
||||
}
|
||||
|
||||
function transition(
|
||||
snapshot: ProfileAssetCustomizationSnapshot,
|
||||
event: ProfileAssetCustomizationEvent,
|
||||
): ProfileAssetCustomizationSnapshot {
|
||||
return transitionProfileAssetCustomizationSnapshot(snapshot, event);
|
||||
}
|
||||
|
||||
function hydrate(remoteState: ProfileAssetRemoteState): ProfileAssetCustomizationSnapshot {
|
||||
return transition(createProfileAssetCustomizationSnapshot(), {type: 'asset.remoteApplied', remoteState});
|
||||
}
|
||||
|
||||
describe('ProfileAssetCustomizationStateMachine', () => {
|
||||
it('derives per-community asset modes from custom hashes and unset flags', () => {
|
||||
expect(
|
||||
createProfileAssetRemoteStateFromFlags({
|
||||
identityKey: IDENTITY,
|
||||
hasCustomAsset: false,
|
||||
isUnset: false,
|
||||
}),
|
||||
).toEqual(remote('inherit', false));
|
||||
expect(
|
||||
createProfileAssetRemoteStateFromFlags({
|
||||
identityKey: IDENTITY,
|
||||
hasCustomAsset: true,
|
||||
isUnset: false,
|
||||
}),
|
||||
).toEqual(remote('custom', true));
|
||||
expect(
|
||||
createProfileAssetRemoteStateFromFlags({
|
||||
identityKey: IDENTITY,
|
||||
hasCustomAsset: true,
|
||||
isUnset: true,
|
||||
}),
|
||||
).toEqual(remote('unset', false));
|
||||
});
|
||||
|
||||
it('tracks an accepted upload as a dirty custom asset patch', () => {
|
||||
let snapshot = hydrate(remote('inherit', false));
|
||||
snapshot = transition(snapshot, {type: 'asset.uploaded', previewUrl: DATA_URL});
|
||||
|
||||
const state = selectProfileAssetCustomizationState(snapshot);
|
||||
expect(state).toMatchObject({
|
||||
state: 'dirty',
|
||||
mode: 'custom',
|
||||
initialMode: 'inherit',
|
||||
hasCustomAsset: true,
|
||||
previewUrl: DATA_URL,
|
||||
hasCleared: false,
|
||||
hasAsset: true,
|
||||
isDirty: true,
|
||||
});
|
||||
expect(getProfileAssetUploadPatch(state)).toEqual({value: DATA_URL});
|
||||
});
|
||||
|
||||
it('sends null when a custom asset is changed back to inherit or unset', () => {
|
||||
let snapshot = hydrate(remote('custom', true));
|
||||
snapshot = transition(snapshot, {type: 'asset.modeSelected', mode: 'inherit'});
|
||||
let state = selectProfileAssetCustomizationState(snapshot);
|
||||
expect(state).toMatchObject({state: 'dirty', mode: 'inherit', hasCleared: false});
|
||||
expect(getProfileAssetUploadPatch(state)).toEqual({value: null});
|
||||
|
||||
snapshot = hydrate(remote('custom', true));
|
||||
snapshot = transition(snapshot, {type: 'asset.modeSelected', mode: 'unset'});
|
||||
state = selectProfileAssetCustomizationState(snapshot);
|
||||
expect(state).toMatchObject({state: 'dirty', mode: 'unset', hasCleared: true});
|
||||
expect(getProfileAssetUploadPatch(state)).toEqual({value: null});
|
||||
});
|
||||
|
||||
it('does not revert an accepted custom upload when a stale same-identity remote snapshot says inherit', () => {
|
||||
let snapshot = hydrate(remote('inherit', false));
|
||||
snapshot = transition(snapshot, {type: 'asset.uploaded', previewUrl: DATA_URL});
|
||||
snapshot = transition(snapshot, {type: 'asset.committed', remoteState: remote('custom', true)});
|
||||
|
||||
let state = selectProfileAssetCustomizationState(snapshot);
|
||||
expect(state).toMatchObject({
|
||||
state: 'committed',
|
||||
mode: 'custom',
|
||||
initialMode: 'custom',
|
||||
hasCustomAsset: true,
|
||||
previewUrl: null,
|
||||
isDirty: false,
|
||||
});
|
||||
|
||||
snapshot = transition(snapshot, {type: 'asset.remoteApplied', remoteState: remote('inherit', false)});
|
||||
state = selectProfileAssetCustomizationState(snapshot);
|
||||
expect(state).toMatchObject({
|
||||
state: 'committed',
|
||||
mode: 'custom',
|
||||
hasCustomAsset: true,
|
||||
isDirty: false,
|
||||
});
|
||||
expect(getProfileAssetUploadPatch(state)).toEqual({value: undefined});
|
||||
|
||||
snapshot = transition(snapshot, {type: 'asset.remoteApplied', remoteState: remote('custom', true)});
|
||||
state = selectProfileAssetCustomizationState(snapshot);
|
||||
expect(state).toMatchObject({state: 'clean', mode: 'custom', hasCustomAsset: true});
|
||||
});
|
||||
|
||||
it('protects dirty local edits from same-identity remote refreshes but accepts identity changes', () => {
|
||||
let snapshot = hydrate(remote('inherit', false));
|
||||
snapshot = transition(snapshot, {type: 'asset.uploaded', previewUrl: DATA_URL});
|
||||
snapshot = transition(snapshot, {type: 'asset.remoteApplied', remoteState: remote('inherit', false)});
|
||||
|
||||
let state = selectProfileAssetCustomizationState(snapshot);
|
||||
expect(state).toMatchObject({state: 'dirty', mode: 'custom', previewUrl: DATA_URL});
|
||||
|
||||
snapshot = transition(snapshot, {
|
||||
type: 'asset.remoteApplied',
|
||||
remoteState: remote('inherit', false, OTHER_IDENTITY),
|
||||
});
|
||||
state = selectProfileAssetCustomizationState(snapshot);
|
||||
expect(state).toMatchObject({
|
||||
state: 'clean',
|
||||
identityKey: OTHER_IDENTITY,
|
||||
mode: 'inherit',
|
||||
previewUrl: null,
|
||||
isDirty: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('allows explicit same-identity resets to discard dirty local edits', () => {
|
||||
let snapshot = hydrate(remote('inherit', false));
|
||||
snapshot = transition(snapshot, {type: 'asset.uploaded', previewUrl: DATA_URL});
|
||||
snapshot = transition(snapshot, {
|
||||
type: 'asset.remoteApplied',
|
||||
remoteState: remote('inherit', false),
|
||||
force: true,
|
||||
});
|
||||
|
||||
const state = selectProfileAssetCustomizationState(snapshot);
|
||||
expect(state).toMatchObject({
|
||||
state: 'clean',
|
||||
mode: 'inherit',
|
||||
previewUrl: null,
|
||||
hasCleared: false,
|
||||
isDirty: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('assigns upload patches without writing undefined fields', () => {
|
||||
const cleanState = selectProfileAssetCustomizationState(hydrate(remote('inherit', false)));
|
||||
const dirtyState = selectProfileAssetCustomizationState(
|
||||
transition(hydrate(remote('inherit', false)), {type: 'asset.uploaded', previewUrl: DATA_URL}),
|
||||
);
|
||||
const payload: {avatar?: string | null} = {};
|
||||
assignProfileAssetUploadPatch(payload, 'avatar', cleanState);
|
||||
expect(payload).toEqual({});
|
||||
assignProfileAssetUploadPatch(payload, 'avatar', dirtyState);
|
||||
expect(payload).toEqual({avatar: DATA_URL});
|
||||
});
|
||||
|
||||
it('models global profile assets as custom or unset instead of inherit', () => {
|
||||
expect(createGlobalProfileAssetRemoteState({identityKey: 'user-1:global', hasCustomAsset: true})).toEqual({
|
||||
identityKey: 'user-1:global',
|
||||
mode: 'custom',
|
||||
hasCustomAsset: true,
|
||||
});
|
||||
expect(createGlobalProfileAssetRemoteState({identityKey: 'user-1:global', hasCustomAsset: false})).toEqual({
|
||||
identityKey: 'user-1:global',
|
||||
mode: 'unset',
|
||||
hasCustomAsset: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {isSettingsItemNew} from '@app/features/user/components/settings_utils/SettingsMetadata';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
const NOW = new Date('2026-06-04T12:00:00.000Z');
|
||||
|
||||
describe('SettingsMetadata new badges', () => {
|
||||
it('does not mark settings without an addedAt timestamp as new', () => {
|
||||
expect(isSettingsItemNew({}, NOW)).toBe(false);
|
||||
});
|
||||
|
||||
it('marks settings added within the freshness window as new', () => {
|
||||
expect(isSettingsItemNew({addedAt: '2026-06-03T00:00:00.000Z'}, NOW)).toBe(true);
|
||||
});
|
||||
|
||||
it('stops marking settings as new after 30 days', () => {
|
||||
expect(isSettingsItemNew({addedAt: '2026-05-01T00:00:00.000Z'}, NOW)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not mark settings as new for accounts created after the setting was added', () => {
|
||||
expect(isSettingsItemNew({addedAt: '2026-06-03T00:00:00.000Z'}, NOW, new Date('2026-06-04T00:00:00.000Z'))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not mark future timestamps as new', () => {
|
||||
expect(isSettingsItemNew({addedAt: '2026-06-05T00:00:00.000Z'}, NOW)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user