Update expo application
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { AiProviderProfileForm } from '../../src/components/settings/ai-provider-profile-form';
|
||||
import { SpoonAgentSettingsForm } from '../../src/components/spoons/spoon-agent-settings-form';
|
||||
import { SpoonSecretsPanel } from '../../src/components/spoons/spoon-secrets-panel';
|
||||
|
||||
describe('mobile forms', () => {
|
||||
test('SpoonSecretsPanel previews secret names only and imports parsed env values', async () => {
|
||||
const onImportSecrets = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
render(
|
||||
<SpoonSecretsPanel
|
||||
adding={false}
|
||||
importing={false}
|
||||
removingId={undefined}
|
||||
secrets={[]}
|
||||
onAddSecret={vi.fn()}
|
||||
onImportSecrets={onImportSecrets}
|
||||
onRemoveSecret={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('AUTH_SECRET=...'), {
|
||||
target: {
|
||||
value: 'AUTH_SECRET=super-secret\nexport AUTHENTIK_CLIENT_ID=client',
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getAllByText(/AUTH_SECRET/).length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText(/AUTHENTIK_CLIENT_ID/).length).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
expect(screen.getByText(/valid secrets found/).textContent).not.toContain(
|
||||
'super-secret',
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Import secrets'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(onImportSecrets).toHaveBeenCalledWith([
|
||||
{ name: 'AUTH_SECRET', value: 'super-secret' },
|
||||
{ name: 'AUTHENTIK_CLIENT_ID', value: 'client' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('SpoonSecretsPanel disables import with no parsed secrets', () => {
|
||||
render(
|
||||
<SpoonSecretsPanel
|
||||
adding={false}
|
||||
importing={false}
|
||||
removingId={undefined}
|
||||
secrets={[]}
|
||||
onAddSecret={vi.fn()}
|
||||
onImportSecrets={vi.fn()}
|
||||
onRemoveSecret={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Import secrets').closest('button')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('AiProviderProfileForm selects default model from model options', async () => {
|
||||
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
render(
|
||||
<AiProviderProfileForm
|
||||
saving={false}
|
||||
onSubmit={onSubmit}
|
||||
existing={{
|
||||
_id: 'profile' as never,
|
||||
authType: 'api_key',
|
||||
defaultModel: 'gpt-5.1-codex',
|
||||
enabled: true,
|
||||
modelOptions: ['gpt-5.1-codex', 'gpt-5.5'],
|
||||
name: 'OpenAI',
|
||||
provider: 'openai',
|
||||
reasoningEffort: 'medium',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('gpt-5.1-codex'));
|
||||
fireEvent.click(screen.getByText('gpt-5.5'));
|
||||
fireEvent.click(screen.getByText('Save provider'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(onSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ defaultModel: 'gpt-5.5' }),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('AiProviderProfileForm shows Codex auth JSON instructions', () => {
|
||||
render(
|
||||
<AiProviderProfileForm
|
||||
saving={false}
|
||||
onSubmit={vi.fn()}
|
||||
existing={{
|
||||
_id: 'profile' as never,
|
||||
authType: 'opencode_auth_json',
|
||||
defaultModel: 'gpt-5.1-codex',
|
||||
enabled: true,
|
||||
modelOptions: ['gpt-5.1-codex'],
|
||||
name: 'Codex',
|
||||
provider: 'opencode_openai_login',
|
||||
reasoningEffort: 'medium',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/~\/.codex\/auth.json/)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('SpoonAgentSettingsForm disables provider/model controls without provider profiles', () => {
|
||||
render(
|
||||
<SpoonAgentSettingsForm
|
||||
profiles={[]}
|
||||
onUpdate={vi.fn()}
|
||||
agent={{
|
||||
agentModel: '',
|
||||
autoDetectCommands: true,
|
||||
branchPrefix: 'spoon/agent',
|
||||
enabled: true,
|
||||
materializeEnvFileByDefault: false,
|
||||
reasoningEffort: 'medium',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText('Configure an AI provider in Settings'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText('No models available').closest('button'),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
test('SpoonAgentSettingsForm applies selected provider defaults', async () => {
|
||||
const onUpdate = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
render(
|
||||
<SpoonAgentSettingsForm
|
||||
agent={{
|
||||
agentModel: 'gpt-5.1-codex',
|
||||
autoDetectCommands: true,
|
||||
branchPrefix: 'spoon/agent',
|
||||
enabled: true,
|
||||
materializeEnvFileByDefault: false,
|
||||
reasoningEffort: 'high',
|
||||
}}
|
||||
profiles={[
|
||||
{
|
||||
_id: 'profile-a' as never,
|
||||
defaultModel: 'gpt-5.1-codex',
|
||||
enabled: true,
|
||||
modelOptions: ['gpt-5.1-codex'],
|
||||
name: 'OpenAI',
|
||||
reasoningEffort: 'medium',
|
||||
},
|
||||
{
|
||||
_id: 'profile-b' as never,
|
||||
defaultModel: 'claude-sonnet-4-5',
|
||||
enabled: true,
|
||||
modelOptions: ['claude-sonnet-4-5'],
|
||||
name: 'Anthropic',
|
||||
reasoningEffort: 'low',
|
||||
},
|
||||
]}
|
||||
onUpdate={onUpdate}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('OpenAI'));
|
||||
fireEvent.click(screen.getByText('Anthropic'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(onUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentModel: 'claude-sonnet-4-5',
|
||||
reasoningEffort: 'low',
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import DashboardRoute from '../../src/app/(app)/dashboard';
|
||||
import SettingsRoute from '../../src/app/(app)/settings';
|
||||
import SpoonsRoute from '../../src/app/(app)/spoons';
|
||||
import ThreadsRoute from '../../src/app/(app)/threads';
|
||||
import WorkspaceRoute from '../../src/app/(app)/workspace/[jobId]';
|
||||
import { mockedUseQuery } from '../setup';
|
||||
|
||||
describe('mobile route smoke tests', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockedUseQuery.mockReset();
|
||||
});
|
||||
|
||||
test('Dashboard renders metrics from mocked Convex data', () => {
|
||||
mockedUseQuery
|
||||
.mockReturnValueOnce([
|
||||
{
|
||||
_id: 'spoon-1',
|
||||
status: 'active',
|
||||
syncStatus: 'behind',
|
||||
upstreamAheadBy: 3,
|
||||
},
|
||||
] as never)
|
||||
.mockReturnValueOnce([] as never)
|
||||
.mockReturnValueOnce([
|
||||
{
|
||||
_id: 'thread-1',
|
||||
source: 'user_request',
|
||||
status: 'open',
|
||||
title: 'Update auth',
|
||||
updatedAt: Date.UTC(2026, 0, 1),
|
||||
},
|
||||
] as never);
|
||||
|
||||
render(<DashboardRoute />);
|
||||
|
||||
expect(screen.getByText('Dashboard')).toBeTruthy();
|
||||
expect(screen.getByText('Update auth')).toBeTruthy();
|
||||
expect(screen.getByText('Upstream commits')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Spoons list renders empty state and one row', () => {
|
||||
mockedUseQuery
|
||||
.mockReturnValueOnce([
|
||||
{
|
||||
_id: 'spoon-1',
|
||||
forkOwner: 'gib',
|
||||
forkRepo: 'usesend',
|
||||
name: 'usesend-authentik',
|
||||
status: 'active',
|
||||
syncStatus: 'up_to_date',
|
||||
upstreamAheadBy: 0,
|
||||
upstreamOwner: 'usesend',
|
||||
upstreamRepo: 'usesend',
|
||||
},
|
||||
] as never)
|
||||
.mockReturnValueOnce([] as never);
|
||||
|
||||
render(<SpoonsRoute />);
|
||||
|
||||
expect(screen.getByText('Spoons')).toBeTruthy();
|
||||
expect(screen.getByText('usesend-authentik')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Threads list renders filters and rows', () => {
|
||||
mockedUseQuery.mockReturnValueOnce([
|
||||
{
|
||||
_id: 'thread-1',
|
||||
source: 'upstream_update',
|
||||
status: 'waiting_for_user',
|
||||
title: 'Upstream auth changes landed',
|
||||
updatedAt: Date.UTC(2026, 0, 1),
|
||||
},
|
||||
] as never);
|
||||
|
||||
render(<ThreadsRoute />);
|
||||
|
||||
expect(screen.getByText('Waiting')).toBeTruthy();
|
||||
expect(screen.getByText('Upstream auth changes landed')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Workspace route renders tabs and job status', () => {
|
||||
mockedUseQuery
|
||||
.mockReturnValueOnce({
|
||||
_id: 'job-1',
|
||||
model: 'gpt-5.1-codex',
|
||||
reasoningEffort: 'medium',
|
||||
status: 'running',
|
||||
workBranch: 'spoon/thread/example',
|
||||
workspaceStatus: 'active',
|
||||
} as never)
|
||||
.mockReturnValueOnce([] as never)
|
||||
.mockReturnValueOnce([] as never)
|
||||
.mockReturnValueOnce([] as never);
|
||||
|
||||
render(<WorkspaceRoute />);
|
||||
|
||||
expect(screen.getByText('Workspace review')).toBeTruthy();
|
||||
expect(screen.getByText('Messages')).toBeTruthy();
|
||||
expect(screen.getByText('running')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Settings index renders GitHub and AI provider summaries', () => {
|
||||
mockedUseQuery
|
||||
.mockReturnValueOnce({ email: 'gib@example.com' } as never)
|
||||
.mockReturnValueOnce({
|
||||
displayName: 'gibbyb',
|
||||
status: 'active',
|
||||
} as never)
|
||||
.mockReturnValueOnce([
|
||||
{
|
||||
_id: 'provider-1',
|
||||
isDefault: true,
|
||||
name: 'Codex',
|
||||
},
|
||||
] as never);
|
||||
|
||||
render(<SettingsRoute />);
|
||||
|
||||
expect(screen.getByText('gib@example.com')).toBeTruthy();
|
||||
expect(screen.getByText('GitHub connected as gibbyb')).toBeTruthy();
|
||||
expect(screen.getByText('1 provider, default Codex')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Alert } from 'react-native';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { SpoonStatusBadge } from '../../src/components/spoons/spoon-status-badge';
|
||||
import { ThreadStatusBadge } from '../../src/components/threads/thread-status-badge';
|
||||
import { ConfirmButton } from '../../src/components/ui/confirm-button';
|
||||
import { PillTabs } from '../../src/components/ui/pill-tabs';
|
||||
import { SheetSelect } from '../../src/components/ui/sheet-select';
|
||||
import { DiffPreview } from '../../src/components/workspace/diff-preview';
|
||||
|
||||
describe('mobile UI primitives', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('PillTabs renders labels and changes selection', () => {
|
||||
const onChange = vi.fn();
|
||||
|
||||
render(
|
||||
<PillTabs
|
||||
tabs={[
|
||||
{ label: 'Overview', value: 'overview' },
|
||||
{ label: 'Settings', value: 'settings' },
|
||||
]}
|
||||
value='overview'
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Settings'));
|
||||
|
||||
expect(screen.getByText('Overview')).toBeTruthy();
|
||||
expect(onChange).toHaveBeenCalledWith('settings');
|
||||
});
|
||||
|
||||
test('SheetSelect opens and chooses an option', () => {
|
||||
const onChange = vi.fn();
|
||||
|
||||
render(
|
||||
<SheetSelect
|
||||
label='Provider'
|
||||
options={[
|
||||
{ label: 'OpenAI', value: 'openai' },
|
||||
{ label: 'Anthropic', value: 'anthropic' },
|
||||
]}
|
||||
value='openai'
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('OpenAI'));
|
||||
fireEvent.click(screen.getByText('Anthropic'));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('anthropic');
|
||||
});
|
||||
|
||||
test('SheetSelect respects disabled state', () => {
|
||||
const onChange = vi.fn();
|
||||
|
||||
render(
|
||||
<SheetSelect
|
||||
disabled
|
||||
label='Provider'
|
||||
options={[{ label: 'OpenAI', value: 'openai' }]}
|
||||
value='openai'
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('OpenAI'));
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('ConfirmButton delegates confirmation to Alert', () => {
|
||||
const onConfirm = vi.fn();
|
||||
|
||||
render(
|
||||
<ConfirmButton
|
||||
confirmLabel='Delete'
|
||||
message='Delete this?'
|
||||
title='Delete'
|
||||
onConfirm={onConfirm}
|
||||
>
|
||||
Remove
|
||||
</ConfirmButton>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Remove'));
|
||||
const calls = vi.mocked(Alert.alert).mock.calls;
|
||||
const confirm = calls[0]?.[2]?.[1];
|
||||
confirm?.onPress?.();
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test('DiffPreview truncates and expands long diffs', () => {
|
||||
const diff = Array.from({ length: 125 }, (_, index) =>
|
||||
index % 2 === 0 ? `+added ${index}` : `-removed ${index}`,
|
||||
).join('\n');
|
||||
|
||||
render(<DiffPreview content={diff} initialLines={3} />);
|
||||
|
||||
expect(screen.getByText('+added 0')).toBeTruthy();
|
||||
expect(screen.queryByText('-removed 5')).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByText('Show 122 more lines'));
|
||||
|
||||
expect(screen.getByText('-removed 5')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('status badges render readable labels', () => {
|
||||
render(
|
||||
<>
|
||||
<SpoonStatusBadge status='up_to_date' />
|
||||
<ThreadStatusBadge status='waiting_for_user' />
|
||||
</>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('up to date')).toBeTruthy();
|
||||
expect(screen.getByText('waiting for user')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
import React from 'react';
|
||||
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
|
||||
import { cleanup } from '@testing-library/react';
|
||||
import { afterEach, vi } from 'vitest';
|
||||
|
||||
Object.defineProperty(globalThis, '__DEV__', {
|
||||
configurable: true,
|
||||
value: false,
|
||||
});
|
||||
|
||||
const createElement =
|
||||
(tag: string) =>
|
||||
({
|
||||
children,
|
||||
onChangeText,
|
||||
onPress,
|
||||
value,
|
||||
...props
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
onChangeText?: (value: string) => void;
|
||||
onPress?: () => void;
|
||||
value?: string;
|
||||
[key: string]: unknown;
|
||||
}) => {
|
||||
const safeProps: Record<string, unknown> = {
|
||||
...props,
|
||||
className:
|
||||
typeof props.className === 'string' ? props.className : undefined,
|
||||
disabled: props.disabled as boolean | undefined,
|
||||
onChange: onChangeText
|
||||
? (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
|
||||
onChangeText(event.currentTarget.value)
|
||||
: undefined,
|
||||
onClick: onPress,
|
||||
value,
|
||||
};
|
||||
delete safeProps.keyboardType;
|
||||
delete safeProps.keyboardShouldPersistTaps;
|
||||
delete safeProps.placeholderTextColor;
|
||||
delete safeProps.secureTextEntry;
|
||||
delete safeProps.showsHorizontalScrollIndicator;
|
||||
delete safeProps.textAlignVertical;
|
||||
|
||||
return React.createElement(tag, safeProps, children);
|
||||
};
|
||||
|
||||
const TextInput = ({
|
||||
multiline,
|
||||
...props
|
||||
}: {
|
||||
multiline?: boolean;
|
||||
[key: string]: unknown;
|
||||
}) => createElement(multiline ? 'textarea' : 'input')(props);
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
alert: vi.fn(),
|
||||
useAction: vi.fn(() => vi.fn()),
|
||||
useMutation: vi.fn(() => vi.fn()),
|
||||
useQuery: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.mock('react-native', () => ({
|
||||
Alert: { alert: mocks.alert },
|
||||
Linking: { openURL: vi.fn() },
|
||||
Modal: ({
|
||||
children,
|
||||
visible,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
visible?: boolean;
|
||||
}) => (visible ? React.createElement('div', {}, children) : null),
|
||||
Pressable: createElement('button'),
|
||||
Platform: {
|
||||
OS: 'web',
|
||||
select: (values: Record<string, unknown>) => values.web ?? values.default,
|
||||
},
|
||||
RefreshControl: createElement('div'),
|
||||
ScrollView: createElement('div'),
|
||||
Switch: createElement('input'),
|
||||
Text: createElement('span'),
|
||||
TextInput,
|
||||
TurboModuleRegistry: {
|
||||
get: vi.fn(() => undefined),
|
||||
getEnforcing: vi.fn(() => ({})),
|
||||
},
|
||||
View: createElement('div'),
|
||||
}));
|
||||
|
||||
vi.mock('expo-clipboard', () => ({
|
||||
setStringAsync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('expo-haptics', () => ({
|
||||
impactAsync: vi.fn(),
|
||||
notificationAsync: vi.fn(),
|
||||
selectionAsync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('react-native-safe-area-context', () => ({
|
||||
SafeAreaView: createElement('div'),
|
||||
}));
|
||||
|
||||
vi.mock('expo-router', () => ({
|
||||
Link: ({ children }: { children?: React.ReactNode }) => children,
|
||||
Stack: {
|
||||
Screen: () => null,
|
||||
},
|
||||
useLocalSearchParams: () => ({}),
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('convex/react', () => ({
|
||||
useAction: mocks.useAction,
|
||||
useMutation: mocks.useMutation,
|
||||
useQuery: mocks.useQuery,
|
||||
}));
|
||||
|
||||
vi.mock('@convex-dev/auth/react', () => ({
|
||||
useAuthActions: () => ({
|
||||
signIn: vi.fn(),
|
||||
signOut: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
export const mockedAlert = mocks.alert;
|
||||
export const mockedUseAction = mocks.useAction;
|
||||
export const mockedUseMutation = mocks.useMutation;
|
||||
export const mockedUseQuery = mocks.useQuery;
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { parseEnvText } from '../../src/utils/env';
|
||||
|
||||
describe('parseEnvText', () => {
|
||||
test('parses dotenv content without exposing invalid rows', () => {
|
||||
expect(
|
||||
parseEnvText(`
|
||||
# comment
|
||||
AUTH_SECRET="secret=value"
|
||||
export authentik_client_id='client'
|
||||
1INVALID=nope
|
||||
EMPTY=
|
||||
`),
|
||||
).toEqual([
|
||||
{ name: 'AUTH_SECRET', value: 'secret=value' },
|
||||
{ name: 'AUTHENTIK_CLIENT_ID', value: 'client' },
|
||||
{ name: 'EMPTY', value: '' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('ignores blank lines and strips matching quotes only', () => {
|
||||
expect(
|
||||
parseEnvText(`
|
||||
|
||||
PLAIN=value
|
||||
QUOTED="value"
|
||||
SINGLE='value'
|
||||
UNMATCHED="value
|
||||
`),
|
||||
).toEqual([
|
||||
{ name: 'PLAIN', value: 'value' },
|
||||
{ name: 'QUOTED', value: 'value' },
|
||||
{ name: 'SINGLE', value: 'value' },
|
||||
{ name: 'UNMATCHED', value: '"value' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
formatDate,
|
||||
formatDateTime,
|
||||
titleize,
|
||||
truncate,
|
||||
} from '../../src/utils/format';
|
||||
|
||||
describe('format utilities', () => {
|
||||
test('formats missing timestamps as never', () => {
|
||||
expect(formatDate(undefined)).toBe('Never');
|
||||
expect(formatDateTime(undefined)).toBe('Never');
|
||||
});
|
||||
|
||||
test('formats known timestamps', () => {
|
||||
const value = Date.UTC(2026, 0, 2, 3, 4, 5);
|
||||
|
||||
expect(formatDate(value)).toContain('2026');
|
||||
expect(formatDateTime(value)).toContain('2026');
|
||||
});
|
||||
|
||||
test('titleizes machine values', () => {
|
||||
expect(titleize('waiting_for_user')).toBe('waiting for user');
|
||||
});
|
||||
|
||||
test('truncates long text', () => {
|
||||
expect(truncate('abcdef', 4)).toBe('a...');
|
||||
expect(truncate('abc', 4)).toBe('abc');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user