mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-19 05:55:34 +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);
|
||||
});
|
||||
});
|
||||
@@ -1,102 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
PROFILE_MODAL_BANNER_AVATAR_CUTOUT,
|
||||
PROFILE_MODAL_GEOMETRY,
|
||||
PROFILE_MODAL_GEOMETRY_STYLE,
|
||||
PROFILE_POPOUT_BANNER_AVATAR_CUTOUT,
|
||||
PROFILE_POPOUT_GEOMETRY,
|
||||
PROFILE_POPOUT_GEOMETRY_STYLE,
|
||||
PROFILE_POPOUT_OUTER_HEIGHT_PX,
|
||||
PROFILE_POPOUT_OUTER_WIDTH_PX,
|
||||
} from '@app/features/user/constants/UserProfileSurfaceGeometry';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
function rem(value: string): number {
|
||||
const match = /^(-?\d+(?:\.\d+)?)rem$/.exec(value);
|
||||
if (!match) {
|
||||
throw new Error(`Expected rem length, got ${value}`);
|
||||
}
|
||||
return Number(match[1]) * 16;
|
||||
}
|
||||
|
||||
describe('PROFILE_POPOUT_GEOMETRY', () => {
|
||||
it('derives the reserved outer width from content width plus both borders', () => {
|
||||
expect(PROFILE_POPOUT_OUTER_WIDTH_PX).toBe(
|
||||
PROFILE_POPOUT_GEOMETRY.contentWidthPx + PROFILE_POPOUT_GEOMETRY.borderWidthPx * 2,
|
||||
);
|
||||
});
|
||||
|
||||
it('derives the reserved outer height from content height plus both borders', () => {
|
||||
expect(PROFILE_POPOUT_OUTER_HEIGHT_PX).toBe(
|
||||
PROFILE_POPOUT_GEOMETRY.contentHeightPx + PROFILE_POPOUT_GEOMETRY.borderWidthPx * 2,
|
||||
);
|
||||
});
|
||||
|
||||
it('exports rem CSS variables that exactly represent the geometry constants', () => {
|
||||
expect(rem(PROFILE_POPOUT_GEOMETRY_STYLE['--profile-popout-content-width'])).toBeCloseTo(
|
||||
PROFILE_POPOUT_GEOMETRY.contentWidthPx,
|
||||
);
|
||||
expect(rem(PROFILE_POPOUT_GEOMETRY_STYLE['--profile-popout-content-height'])).toBeCloseTo(
|
||||
PROFILE_POPOUT_GEOMETRY.contentHeightPx,
|
||||
);
|
||||
expect(rem(PROFILE_POPOUT_GEOMETRY_STYLE['--profile-popout-border-width'])).toBeCloseTo(
|
||||
PROFILE_POPOUT_GEOMETRY.borderWidthPx,
|
||||
);
|
||||
expect(rem(PROFILE_POPOUT_GEOMETRY_STYLE['--profile-popout-avatar-size'])).toBeCloseTo(
|
||||
PROFILE_POPOUT_GEOMETRY.avatarSizePx,
|
||||
);
|
||||
expect(rem(PROFILE_POPOUT_GEOMETRY_STYLE['--profile-popout-avatar-border'])).toBeCloseTo(
|
||||
PROFILE_POPOUT_GEOMETRY.avatarBorderPx,
|
||||
);
|
||||
expect(rem(PROFILE_POPOUT_GEOMETRY_STYLE['--profile-popout-banner-height'])).toBeCloseTo(
|
||||
PROFILE_POPOUT_GEOMETRY.bannerHeightPx,
|
||||
);
|
||||
expect(rem(PROFILE_POPOUT_GEOMETRY_STYLE['--profile-popout-header-height'])).toBeCloseTo(
|
||||
PROFILE_POPOUT_GEOMETRY.headerHeightPx,
|
||||
);
|
||||
});
|
||||
|
||||
it('emits only rem lengths for both profile geometry surfaces', () => {
|
||||
for (const value of Object.values(PROFILE_POPOUT_GEOMETRY_STYLE)) {
|
||||
expect(value).toMatch(/^-?\d+(?:\.\d+)?rem$/);
|
||||
}
|
||||
for (const value of Object.values(PROFILE_MODAL_GEOMETRY_STYLE)) {
|
||||
expect(value).toMatch(/^-?\d+(?:\.\d+)?rem$/);
|
||||
}
|
||||
});
|
||||
|
||||
it('represents the modal geometry constants as rem CSS variables', () => {
|
||||
expect(rem(PROFILE_MODAL_GEOMETRY_STYLE['--profile-modal-avatar-size'])).toBeCloseTo(
|
||||
PROFILE_MODAL_GEOMETRY.avatarSizePx,
|
||||
);
|
||||
expect(rem(PROFILE_MODAL_GEOMETRY_STYLE['--profile-modal-avatar-border'])).toBeCloseTo(
|
||||
PROFILE_MODAL_GEOMETRY.avatarBorderPx,
|
||||
);
|
||||
expect(rem(PROFILE_MODAL_GEOMETRY_STYLE['--profile-modal-avatar-left'])).toBeCloseTo(
|
||||
PROFILE_MODAL_GEOMETRY.avatarLeftPx,
|
||||
);
|
||||
expect(rem(PROFILE_MODAL_GEOMETRY_STYLE['--profile-modal-avatar-top'])).toBeCloseTo(
|
||||
PROFILE_MODAL_GEOMETRY.avatarTopPx,
|
||||
);
|
||||
});
|
||||
|
||||
it('proves the skeleton loading shell reserves the declared content rectangle plus both borders', () => {
|
||||
const skeletonOuterWidth = PROFILE_POPOUT_GEOMETRY.contentWidthPx + PROFILE_POPOUT_GEOMETRY.borderWidthPx * 2;
|
||||
const skeletonOuterHeight = PROFILE_POPOUT_GEOMETRY.contentHeightPx + PROFILE_POPOUT_GEOMETRY.borderWidthPx * 2;
|
||||
|
||||
expect(skeletonOuterWidth).toBe(PROFILE_POPOUT_OUTER_WIDTH_PX);
|
||||
expect(skeletonOuterHeight).toBe(PROFILE_POPOUT_OUTER_HEIGHT_PX);
|
||||
});
|
||||
|
||||
it('uses avatar status gutter widths for profile banner avatar cutouts', () => {
|
||||
expect(PROFILE_POPOUT_GEOMETRY.avatarBorderPx).toBe(3.2);
|
||||
expect(PROFILE_MODAL_GEOMETRY.avatarBorderPx).toBe(4.8);
|
||||
expect(PROFILE_POPOUT_BANNER_AVATAR_CUTOUT.r).toBe(
|
||||
PROFILE_POPOUT_GEOMETRY.avatarSizePx / 2 + PROFILE_POPOUT_GEOMETRY.avatarBorderPx,
|
||||
);
|
||||
expect(PROFILE_MODAL_BANNER_AVATAR_CUTOUT.r).toBe(
|
||||
PROFILE_MODAL_GEOMETRY.avatarSizePx / 2 + PROFILE_MODAL_GEOMETRY.avatarBorderPx,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import AppStorage from '@app/features/platform/state/PersistentStorage';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
const KEEP_ATTACHMENTS_ON_EMPTY_MESSAGE_EDIT_STORAGE_KEY = 'AdvancedSettings:keepAttachmentsOnEmptyMessageEdit';
|
||||
|
||||
AppStorage.setItem(KEEP_ATTACHMENTS_ON_EMPTY_MESSAGE_EDIT_STORAGE_KEY, JSON.stringify(true));
|
||||
|
||||
const {default: AdvancedSettings} = await import('@app/features/user/state/AdvancedSettings');
|
||||
|
||||
describe('Advanced settings', () => {
|
||||
it('rehydrates the stored attachment edit preference', () => {
|
||||
expect(AdvancedSettings.keepAttachmentsOnEmptyMessageEdit).toBe(true);
|
||||
});
|
||||
|
||||
it('stores the attachment edit preference under its own key', () => {
|
||||
AdvancedSettings.setKeepAttachmentsOnEmptyMessageEdit(false);
|
||||
expect(AppStorage.getItem(KEEP_ATTACHMENTS_ON_EMPTY_MESSAGE_EDIT_STORAGE_KEY)).toBe('false');
|
||||
expect(AdvancedSettings.unreadBadgeCustomizationEnabled).toBe(false);
|
||||
expect(AppStorage.getItem('AdvancedSettings:unreadBadgeCustomizationEnabled')).toBeNull();
|
||||
});
|
||||
|
||||
it('defaults the expression clone shortcuts preference to off and stores it under its own key', () => {
|
||||
expect(AdvancedSettings.expressionCloneShortcutsEnabled).toBe(false);
|
||||
AdvancedSettings.setExpressionCloneShortcutsEnabled(true);
|
||||
expect(AppStorage.getItem('AdvancedSettings:expressionCloneShortcutsEnabled')).toBe('true');
|
||||
});
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {decideOversizePush} from '@app/features/user/state/SyncedFieldBudget';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
const MAX = 1000;
|
||||
|
||||
describe('decideOversizePush', () => {
|
||||
it('accepts a payload inside the budget', () => {
|
||||
expect(decideOversizePush({encodedBytes: 999, maxEncodedBytes: MAX, lastPreparedBytes: null})).toBe(
|
||||
'within-budget',
|
||||
);
|
||||
expect(decideOversizePush({encodedBytes: MAX, maxEncodedBytes: MAX, lastPreparedBytes: null})).toBe(
|
||||
'within-budget',
|
||||
);
|
||||
});
|
||||
|
||||
it('drops the first over-budget payload', () => {
|
||||
expect(decideOversizePush({encodedBytes: 1001, maxEncodedBytes: MAX, lastPreparedBytes: null})).toBe('drop');
|
||||
});
|
||||
|
||||
it('drops an over-budget payload that grows', () => {
|
||||
expect(decideOversizePush({encodedBytes: 1200, maxEncodedBytes: MAX, lastPreparedBytes: 1100})).toBe('drop');
|
||||
});
|
||||
|
||||
it('drops an over-budget payload that stays the same size', () => {
|
||||
expect(decideOversizePush({encodedBytes: 1100, maxEncodedBytes: MAX, lastPreparedBytes: 1100})).toBe('drop');
|
||||
});
|
||||
|
||||
it('pushes an over-budget payload that shrinks so removals persist', () => {
|
||||
expect(decideOversizePush({encodedBytes: 1050, maxEncodedBytes: MAX, lastPreparedBytes: 1100})).toBe(
|
||||
'push-shrinks',
|
||||
);
|
||||
});
|
||||
|
||||
it('lets a run of removals drain back under the budget', () => {
|
||||
let last: number | null = null;
|
||||
const decisions: Array<string> = [];
|
||||
for (const bytes of [1300, 1200, 1100, 900]) {
|
||||
decisions.push(decideOversizePush({encodedBytes: bytes, maxEncodedBytes: MAX, lastPreparedBytes: last}));
|
||||
last = bytes;
|
||||
}
|
||||
expect(decisions).toEqual(['drop', 'push-shrinks', 'push-shrinks', 'within-budget']);
|
||||
});
|
||||
});
|
||||
@@ -1,177 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
createSyncedFieldMachineSnapshot,
|
||||
type SyncedFieldMachineEvent,
|
||||
type SyncedFieldMachineSnapshot,
|
||||
selectSyncedFieldMachineModel,
|
||||
transitionSyncedFieldMachineSnapshot,
|
||||
} from '@app/features/user/state/SyncedFieldStateMachine';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
const REMOTE_SNAPSHOT = Object.freeze({source: 'remote'});
|
||||
const LOCAL_CANDIDATE = Object.freeze({source: 'local'});
|
||||
|
||||
function transition(snapshot: SyncedFieldMachineSnapshot, event: SyncedFieldMachineEvent): SyncedFieldMachineSnapshot {
|
||||
return transitionSyncedFieldMachineSnapshot(snapshot, event);
|
||||
}
|
||||
|
||||
describe('SyncedFieldStateMachine', () => {
|
||||
it('starts active and idle without commands', () => {
|
||||
const model = selectSyncedFieldMachineModel(createSyncedFieldMachineSnapshot());
|
||||
expect(model.state).toBe('idle');
|
||||
expect(model.isActive).toBe(true);
|
||||
expect(model.command).toBeNull();
|
||||
expect(model.suppressPush).toBe(false);
|
||||
expect(model.failure).toBeNull();
|
||||
});
|
||||
|
||||
it('skips missing or disabled remote snapshots', () => {
|
||||
let snapshot = createSyncedFieldMachineSnapshot();
|
||||
snapshot = transition(snapshot, {
|
||||
type: 'sync.remoteObserved',
|
||||
enabled: true,
|
||||
snapshot: undefined,
|
||||
});
|
||||
expect(selectSyncedFieldMachineModel(snapshot).command).toBeNull();
|
||||
|
||||
snapshot = transition(snapshot, {
|
||||
type: 'sync.remoteObserved',
|
||||
enabled: false,
|
||||
snapshot: REMOTE_SNAPSHOT,
|
||||
});
|
||||
const model = selectSyncedFieldMachineModel(snapshot);
|
||||
expect(model.state).toBe('idle');
|
||||
expect(model.command).toBeNull();
|
||||
});
|
||||
|
||||
it('compares a remote snapshot before deciding whether to apply it', () => {
|
||||
let snapshot = createSyncedFieldMachineSnapshot();
|
||||
snapshot = transition(snapshot, {
|
||||
type: 'sync.remoteObserved',
|
||||
enabled: true,
|
||||
snapshot: REMOTE_SNAPSHOT,
|
||||
});
|
||||
|
||||
let model = selectSyncedFieldMachineModel(snapshot);
|
||||
expect(model.state).toBe('comparingRemote');
|
||||
expect(model.command).toEqual({type: 'compareRemote', snapshot: REMOTE_SNAPSHOT});
|
||||
|
||||
snapshot = transition(snapshot, {type: 'sync.remoteMatched'});
|
||||
model = selectSyncedFieldMachineModel(snapshot);
|
||||
expect(model.state).toBe('idle');
|
||||
expect(model.command).toBeNull();
|
||||
});
|
||||
|
||||
it('suppresses local pushes while a remote snapshot is being applied', () => {
|
||||
let snapshot = createSyncedFieldMachineSnapshot();
|
||||
snapshot = transition(snapshot, {
|
||||
type: 'sync.remoteObserved',
|
||||
enabled: true,
|
||||
snapshot: REMOTE_SNAPSHOT,
|
||||
});
|
||||
snapshot = transition(snapshot, {type: 'sync.remoteNeedsApply'});
|
||||
|
||||
let model = selectSyncedFieldMachineModel(snapshot);
|
||||
expect(model.state).toBe('applyingRemote');
|
||||
expect(model.suppressPush).toBe(true);
|
||||
expect(model.command).toEqual({type: 'applyRemote', snapshot: REMOTE_SNAPSHOT});
|
||||
|
||||
snapshot = transition(snapshot, {
|
||||
type: 'sync.localObserved',
|
||||
enabled: true,
|
||||
candidate: LOCAL_CANDIDATE,
|
||||
});
|
||||
model = selectSyncedFieldMachineModel(snapshot);
|
||||
expect(model.state).toBe('applyingRemote');
|
||||
expect(model.command).toEqual({type: 'applyRemote', snapshot: REMOTE_SNAPSHOT});
|
||||
|
||||
snapshot = transition(snapshot, {type: 'sync.applyFinished'});
|
||||
model = selectSyncedFieldMachineModel(snapshot);
|
||||
expect(model.state).toBe('idle');
|
||||
expect(model.suppressPush).toBe(false);
|
||||
expect(model.command).toBeNull();
|
||||
});
|
||||
|
||||
it('prepares and pushes a changed local candidate', () => {
|
||||
let snapshot = createSyncedFieldMachineSnapshot();
|
||||
snapshot = transition(snapshot, {
|
||||
type: 'sync.localObserved',
|
||||
enabled: true,
|
||||
candidate: LOCAL_CANDIDATE,
|
||||
});
|
||||
|
||||
let model = selectSyncedFieldMachineModel(snapshot);
|
||||
expect(model.state).toBe('preparingPush');
|
||||
expect(model.command).toEqual({type: 'preparePush', candidate: LOCAL_CANDIDATE});
|
||||
|
||||
snapshot = transition(snapshot, {type: 'sync.localReadyToPush'});
|
||||
model = selectSyncedFieldMachineModel(snapshot);
|
||||
expect(model.state).toBe('pushing');
|
||||
expect(model.command).toEqual({type: 'pushLocal', candidate: LOCAL_CANDIDATE});
|
||||
|
||||
snapshot = transition(snapshot, {type: 'sync.commandHandled'});
|
||||
model = selectSyncedFieldMachineModel(snapshot);
|
||||
expect(model.state).toBe('idle');
|
||||
expect(model.command).toBeNull();
|
||||
});
|
||||
|
||||
it('skips local candidates that are disabled, absent, or already synced', () => {
|
||||
let snapshot = createSyncedFieldMachineSnapshot();
|
||||
snapshot = transition(snapshot, {
|
||||
type: 'sync.localObserved',
|
||||
enabled: false,
|
||||
candidate: LOCAL_CANDIDATE,
|
||||
});
|
||||
expect(selectSyncedFieldMachineModel(snapshot).command).toBeNull();
|
||||
|
||||
snapshot = transition(snapshot, {
|
||||
type: 'sync.localObserved',
|
||||
enabled: true,
|
||||
candidate: null,
|
||||
});
|
||||
expect(selectSyncedFieldMachineModel(snapshot).command).toBeNull();
|
||||
|
||||
snapshot = transition(snapshot, {
|
||||
type: 'sync.localObserved',
|
||||
enabled: true,
|
||||
candidate: LOCAL_CANDIDATE,
|
||||
});
|
||||
snapshot = transition(snapshot, {type: 'sync.localAlreadySynced'});
|
||||
|
||||
const model = selectSyncedFieldMachineModel(snapshot);
|
||||
expect(model.state).toBe('idle');
|
||||
expect(model.command).toBeNull();
|
||||
});
|
||||
|
||||
it('suspends on terminal failures and ignores further sync events until reset', () => {
|
||||
let snapshot = createSyncedFieldMachineSnapshot();
|
||||
snapshot = transition(snapshot, {
|
||||
type: 'sync.failed',
|
||||
failure: {
|
||||
reason: 'payload-too-large',
|
||||
message: 'too large',
|
||||
},
|
||||
});
|
||||
|
||||
let model = selectSyncedFieldMachineModel(snapshot);
|
||||
expect(model.state).toBe('suspended');
|
||||
expect(model.isActive).toBe(false);
|
||||
expect(model.failure?.reason).toBe('payload-too-large');
|
||||
expect(model.command).toBeNull();
|
||||
|
||||
snapshot = transition(snapshot, {
|
||||
type: 'sync.localObserved',
|
||||
enabled: true,
|
||||
candidate: LOCAL_CANDIDATE,
|
||||
});
|
||||
expect(selectSyncedFieldMachineModel(snapshot).state).toBe('suspended');
|
||||
expect(selectSyncedFieldMachineModel(snapshot).command).toBeNull();
|
||||
|
||||
snapshot = transition(snapshot, {type: 'sync.reset'});
|
||||
model = selectSyncedFieldMachineModel(snapshot);
|
||||
expect(model.state).toBe('idle');
|
||||
expect(model.isActive).toBe(true);
|
||||
expect(model.failure).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
resolveUnreadInboxVisibility,
|
||||
type UnreadInboxVisibilityInput,
|
||||
} from '@app/features/user/state/UnreadInboxVisibility';
|
||||
import {MessageNotifications} from '@fluxer/constants/src/NotificationConstants';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
function input(overrides: Partial<UnreadInboxVisibilityInput> = {}): UnreadInboxVisibilityInput {
|
||||
return {
|
||||
unreadBadgesLevel: null,
|
||||
isMuted: false,
|
||||
hasUnread: true,
|
||||
hasMentions: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('resolveUnreadInboxVisibility', () => {
|
||||
it('shows an unmuted channel with plain unread messages', () => {
|
||||
expect(resolveUnreadInboxVisibility(input())).toBe(true);
|
||||
});
|
||||
|
||||
it('shows a channel in a large community, where notification level is forced to only-mentions', () => {
|
||||
expect(resolveUnreadInboxVisibility(input({unreadBadgesLevel: null, hasMentions: false}))).toBe(true);
|
||||
});
|
||||
|
||||
it('hides a channel with nothing unread and no mentions', () => {
|
||||
expect(resolveUnreadInboxVisibility(input({hasUnread: false, hasMentions: false}))).toBe(false);
|
||||
});
|
||||
|
||||
it('hides plain unread in a muted channel', () => {
|
||||
expect(resolveUnreadInboxVisibility(input({isMuted: true}))).toBe(false);
|
||||
});
|
||||
|
||||
it('shows a muted channel when it has a mention', () => {
|
||||
expect(resolveUnreadInboxVisibility(input({isMuted: true, hasMentions: true}))).toBe(true);
|
||||
});
|
||||
|
||||
it('hides a channel whose explicit unread badges level is nothing, even with a mention', () => {
|
||||
expect(
|
||||
resolveUnreadInboxVisibility(input({unreadBadgesLevel: MessageNotifications.NO_MESSAGES, hasMentions: true})),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('hides plain unread when the explicit unread badges level is only-mentions', () => {
|
||||
expect(resolveUnreadInboxVisibility(input({unreadBadgesLevel: MessageNotifications.ONLY_MENTIONS}))).toBe(false);
|
||||
});
|
||||
|
||||
it('shows a mention when the explicit unread badges level is only-mentions', () => {
|
||||
expect(
|
||||
resolveUnreadInboxVisibility(input({unreadBadgesLevel: MessageNotifications.ONLY_MENTIONS, hasMentions: true})),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('shows plain unread when the explicit unread badges level is all messages', () => {
|
||||
expect(resolveUnreadInboxVisibility(input({unreadBadgesLevel: MessageNotifications.ALL_MESSAGES}))).toBe(true);
|
||||
});
|
||||
|
||||
it('takes no notification-level input at all, so a forced community level cannot gate it', () => {
|
||||
const keys = Object.keys(input()).sort();
|
||||
expect(keys).toEqual(['hasMentions', 'hasUnread', 'isMuted', 'unreadBadgesLevel']);
|
||||
});
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {Profile} from '@app/features/user/models/Profile';
|
||||
import {afterEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
vi.mock('@app/features/auth/state/Authentication', () => ({default: {currentUserId: null}}));
|
||||
|
||||
const {default: UserProfile} = await import('@app/features/user/state/UserProfile');
|
||||
|
||||
const GUILD_ID = '1000';
|
||||
const TARGET_ID = '2000';
|
||||
|
||||
function cache(userId: string = TARGET_ID, guildId?: string): void {
|
||||
UserProfile.handleProfileCreate({
|
||||
userId,
|
||||
guildId: guildId ?? null,
|
||||
mutualGuilds: [{id: GUILD_ID, nick: null}],
|
||||
} as unknown as Profile);
|
||||
}
|
||||
|
||||
describe('UserProfile cache invalidation', () => {
|
||||
afterEach(() => {
|
||||
UserProfile.handleGatewayReady();
|
||||
});
|
||||
|
||||
it('drops every cached scope for a member removed from a community', () => {
|
||||
cache();
|
||||
cache(TARGET_ID, GUILD_ID);
|
||||
expect(UserProfile.getProfile(TARGET_ID)?.mutualGuilds).toHaveLength(1);
|
||||
UserProfile.handleGuildMemberRemove(TARGET_ID);
|
||||
expect(UserProfile.getProfile(TARGET_ID)).toBeNull();
|
||||
expect(UserProfile.getProfile(TARGET_ID, GUILD_ID)).toBeNull();
|
||||
});
|
||||
|
||||
it('drops every cached scope for a member who joined a community', () => {
|
||||
cache();
|
||||
UserProfile.handleGuildMemberAdd(TARGET_ID);
|
||||
expect(UserProfile.getProfile(TARGET_ID)).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps other users cached when one member is removed', () => {
|
||||
cache();
|
||||
cache('3000');
|
||||
UserProfile.handleGuildMemberRemove(TARGET_ID);
|
||||
expect(UserProfile.getProfile(TARGET_ID)).toBeNull();
|
||||
expect(UserProfile.getProfile('3000')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('drops cached profiles when the viewer leaves a community', () => {
|
||||
cache();
|
||||
UserProfile.handleGuildDelete(false);
|
||||
expect(UserProfile.getProfile(TARGET_ID)).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps cached profiles when a community only goes unavailable', () => {
|
||||
cache();
|
||||
UserProfile.handleGuildDelete(true);
|
||||
expect(UserProfile.getProfile(TARGET_ID)).not.toBeNull();
|
||||
});
|
||||
|
||||
it('drops cached profiles when the viewer joins a community', () => {
|
||||
cache();
|
||||
UserProfile.handleGuildCreate();
|
||||
expect(UserProfile.getProfile(TARGET_ID)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {isVisionaryDiscriminator0000Blocked} from '@app/features/user/utils/FluxerTagDiscriminatorUtils';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
describe('isVisionaryDiscriminator0000Blocked', () => {
|
||||
it('allows #0000 on self-hosted instances', () => {
|
||||
expect(
|
||||
isVisionaryDiscriminator0000Blocked({
|
||||
showPremium: false,
|
||||
isVisionary: false,
|
||||
discriminator: '0000',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('blocks #0000 for non-visionary users on the main instance', () => {
|
||||
expect(
|
||||
isVisionaryDiscriminator0000Blocked({
|
||||
showPremium: true,
|
||||
isVisionary: false,
|
||||
discriminator: '0000',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('allows #0000 for visionary users on the main instance', () => {
|
||||
expect(
|
||||
isVisionaryDiscriminator0000Blocked({
|
||||
showPremium: true,
|
||||
isVisionary: true,
|
||||
discriminator: '0000',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('allows other discriminators on the main instance', () => {
|
||||
expect(
|
||||
isVisionaryDiscriminator0000Blocked({
|
||||
showPremium: true,
|
||||
isVisionary: false,
|
||||
discriminator: '1337',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user