test: raise coverage to 99 percent

This commit is contained in:
xixu-me committed 2026-04-09 00:43:58 +08:00
1 parent 42bd6e2b3a
commit 33c73cf620
14 files changed
+646 -73

No files matched your search

@@ -1,8 +1,14 @@
package repo
import (
"context"
"errors"
"testing"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/stretchr/testify/require"
)
@@ -15,3 +21,124 @@ func TestNewPostgresRepositoryWrapsPoolBackedDB(t *testing.T) {
require.True(t, ok)
require.Nil(t, wrapped.pool)
}
type wrapperStubRow struct {
err error
}
func (row wrapperStubRow) Scan(dest ...any) error {
_ = dest
return row.err
}
type wrapperStubTx struct {
commitErr error
rollbackErr error
execErr error
queryRow pgx.Row
execSQL string
queryRowSQL string
execArgs []any
queryRowArgs []any
}
func (tx *wrapperStubTx) Begin(context.Context) (pgx.Tx, error) {
return tx, nil
}
func (tx *wrapperStubTx) Commit(context.Context) error {
return tx.commitErr
}
func (tx *wrapperStubTx) Rollback(context.Context) error {
return tx.rollbackErr
}
func (tx *wrapperStubTx) CopyFrom(context.Context, pgx.Identifier, []string, pgx.CopyFromSource) (int64, error) {
panic("unexpected CopyFrom call")
}
func (tx *wrapperStubTx) SendBatch(context.Context, *pgx.Batch) pgx.BatchResults {
panic("unexpected SendBatch call")
}
func (tx *wrapperStubTx) LargeObjects() pgx.LargeObjects {
return pgx.LargeObjects{}
}
func (tx *wrapperStubTx) Prepare(context.Context, string, string) (*pgconn.StatementDescription, error) {
panic("unexpected Prepare call")
}
func (tx *wrapperStubTx) Exec(_ context.Context, sql string, arguments ...any) (pgconn.CommandTag, error) {
tx.execSQL = sql
tx.execArgs = arguments
return pgconn.CommandTag{}, tx.execErr
}
func (tx *wrapperStubTx) Query(context.Context, string, ...any) (pgx.Rows, error) {
panic("unexpected Query call")
}
func (tx *wrapperStubTx) QueryRow(_ context.Context, sql string, args ...any) pgx.Row {
tx.queryRowSQL = sql
tx.queryRowArgs = args
return tx.queryRow
}
func (tx *wrapperStubTx) Conn() *pgx.Conn {
return nil
}
func TestPgxTxDelegatesToTheUnderlyingTransaction(t *testing.T) {
t.Parallel()
ctx := context.Background()
row := wrapperStubRow{err: errors.New("scan failed")}
tx := &wrapperStubTx{
commitErr: errors.New("commit failed"),
rollbackErr: errors.New("rollback failed"),
execErr: errors.New("exec failed"),
queryRow: row,
}
wrapped := pgxTx{tx: tx}
require.ErrorIs(t, wrapped.Commit(ctx), tx.commitErr)
require.ErrorIs(t, wrapped.Exec(ctx, "select 1", "arg"), tx.execErr)
require.Equal(t, "select 1", tx.execSQL)
require.Equal(t, []any{"arg"}, tx.execArgs)
require.Equal(t, row, wrapped.QueryRow(ctx, "select 2", 42))
require.Equal(t, "select 2", tx.queryRowSQL)
require.Equal(t, []any{42}, tx.queryRowArgs)
require.ErrorIs(t, wrapped.Rollback(ctx), tx.rollbackErr)
}
func TestPgxPoolDBMethodsDelegateThroughThePool(t *testing.T) {
t.Parallel()
config, err := pgxpool.ParseConfig("postgres://xdrop:xdrop@127.0.0.1:1/xdrop?sslmode=disable")
require.NoError(t, err)
config.ConnConfig.ConnectTimeout = 20 * time.Millisecond
pool, err := pgxpool.NewWithConfig(context.Background(), config)
require.NoError(t, err)
t.Cleanup(pool.Close)
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
wrapped := pgxPoolDB{pool: pool}
_, err = wrapped.Begin(ctx)
require.Error(t, err)
err = wrapped.Exec(ctx, "select 1")
require.Error(t, err)
_, err = wrapped.Query(ctx, "select 1")
require.Error(t, err)
row := wrapped.QueryRow(ctx, "select 1")
require.NotNil(t, row)
require.Error(t, row.Scan(new(int)))
}
+75 -1
View File
@@ -1,6 +1,12 @@
package testutil
import "testing"
import (
"context"
"errors"
"strings"
"testing"
"time"
)
func TestParsePublishedPortParsesIPv4Binding(t *testing.T) {
t.Parallel()
@@ -33,3 +39,71 @@ func TestParsePublishedPortRejectsUnexpectedFormat(t *testing.T) {
t.Fatal("parsePublishedPort succeeded for malformed output")
}
}
func TestParsePublishedPortUsesTheFirstLineFromMultiLineOutput(t *testing.T) {
t.Parallel()
port, err := parsePublishedPort("127.0.0.1:49154\n[::]:32768\n")
if err != nil {
t.Fatalf("parsePublishedPort returned error: %v", err)
}
if port != "49154" {
t.Fatalf("parsePublishedPort returned %q, want %q", port, "49154")
}
}
func TestWaitForConditionRetriesUntilSuccess(t *testing.T) {
t.Parallel()
attempts := 0
err := WaitForCondition(context.Background(), 50*time.Millisecond, time.Millisecond, func() error {
attempts++
if attempts < 3 {
return errors.New("not yet")
}
return nil
})
if err != nil {
t.Fatalf("WaitForCondition returned error: %v", err)
}
if attempts != 3 {
t.Fatalf("WaitForCondition attempted %d times, want 3", attempts)
}
}
func TestWaitForConditionReturnsContextError(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
cancel()
err := WaitForCondition(ctx, 50*time.Millisecond, time.Millisecond, func() error {
return errors.New("still waiting")
})
if !errors.Is(err, context.Canceled) {
t.Fatalf("WaitForCondition returned %v, want context.Canceled", err)
}
}
func TestWaitForConditionIncludesLastErrorOnTimeout(t *testing.T) {
t.Parallel()
err := WaitForCondition(context.Background(), 5*time.Millisecond, time.Millisecond, func() error {
return errors.New("still failing")
})
if err == nil {
t.Fatal("WaitForCondition succeeded, want timeout error")
}
if !strings.Contains(err.Error(), "still failing") {
t.Fatalf("WaitForCondition returned %q, want to include the last error", err)
}
}
func TestUniqueContainerNameFallsBackToTheDefaultPrefix(t *testing.T) {
t.Parallel()
name := uniqueContainerName(" ")
if !strings.HasPrefix(name, "xdrop-test-") {
t.Fatalf("uniqueContainerName returned %q, want default prefix", name)
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:coverage": "vitest run --coverage --maxWorkers=1",
"test:watch": "vitest",
"typecheck": "tsc --noEmit -p tsconfig.app.json"
},
@@ -159,4 +159,44 @@ describe('HistoryBoard', () => {
expect(screen.getByText('Could not update this transfer right now.')).toBeInTheDocument()
expect(deleteTransferMock).toHaveBeenCalledWith('ready-1')
})
it('lets local-only transfers cancel a pending forget action and clears stale errors on retry', async () => {
transfersState.splice(
0,
transfersState.length,
createTransfer('local-only-2', 'Forgotten transfer', '2026-03-21T10:00:00.000Z', {
manageToken: '',
}),
)
deleteTransferMock.mockRejectedValueOnce(new Error('Delete failed'))
deleteTransferMock.mockResolvedValueOnce(undefined)
renderBoard()
fireEvent.click(screen.getByRole('button', { name: 'Forget local copy' }))
expect(screen.getByText(/Confirm forget to remove this local record/i)).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Confirm forget' }))
await act(async () => {
await Promise.resolve()
})
expect(screen.getByText('Delete failed')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
expect(screen.queryByRole('button', { name: 'Confirm forget' })).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Forget local copy' }))
expect(screen.queryByText('Delete failed')).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Confirm forget' }))
await act(async () => {
await Promise.resolve()
})
expect(deleteTransferMock).toHaveBeenCalledTimes(2)
expect(screen.queryByRole('button', { name: 'Confirm forget' })).not.toBeInTheDocument()
})
})
@@ -65,7 +65,7 @@ export function HistoryBoard() {
clearActionError(transferId)
try {
await deleteTransfer(transferId)
setPendingDeleteTransferId((current) => (current === transferId ? undefined : current))
setPendingDeleteTransferId(undefined)
} catch (error) {
setActionError(transferId, error)
}
@@ -170,11 +170,7 @@ export function HistoryBoard() {
</Button>
<Button
tone="ghost"
onClick={() =>
setPendingDeleteTransferId((current) =>
current === transfer.id ? undefined : current,
)
}
onClick={() => setPendingDeleteTransferId(undefined)}
>
Cancel
</Button>
@@ -212,6 +212,37 @@ describe('ShareCard', () => {
expect(screen.getByRole('button', { name: 'Copied' })).toBeInTheDocument()
})
it('falls back to copying when the share sheet fails for non-abort reasons', async () => {
const shareMock = vi.fn(async () => {
throw new Error('share failed')
})
Object.assign(navigator, { share: shareMock })
render(
<MemoryRouter>
<ShareCard
transfer={createTransfer('active-4b', 'Fallback transfer', '2026-03-21T10:00:00.000Z')}
onExtendTransfer={extendTransferMock}
/>
</MemoryRouter>,
)
await act(async () => {
await Promise.resolve()
})
fireEvent.click(screen.getByRole('button', { name: 'Share link' }))
await act(async () => {
await Promise.resolve()
})
expect(shareMock).toHaveBeenCalledTimes(1)
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
'https://example.com/t/active-4b#k=test',
)
})
it('shows a retry-focused fallback when clipboard access fails', async () => {
Object.assign(navigator, {
clipboard: {
@@ -432,6 +463,43 @@ describe('ShareCard', () => {
).not.toBeInTheDocument()
})
it('guards copy and share handlers when a transfer has no share link', async () => {
render(
<MemoryRouter>
<ShareCard
transfer={createTransfer('active-10', 'Missing share link', '2026-03-21T10:00:00.000Z', {
shareUrl: '',
})}
onExtendTransfer={extendTransferMock}
/>
</MemoryRouter>,
)
await act(async () => {
await Promise.resolve()
})
const copyButton = screen.getByRole('button', { name: 'Copy link' }) as HTMLButtonElement
const shareButton = screen.getByRole('button', { name: 'Share link' }) as HTMLButtonElement
expect(copyButton).toBeDisabled()
expect(shareButton).toBeDisabled()
copyButton.disabled = false
shareButton.disabled = false
fireEvent.click(copyButton)
fireEvent.click(shareButton)
await act(async () => {
await Promise.resolve()
})
expect(navigator.clipboard.writeText).not.toHaveBeenCalled()
expect(toDataUrlMock).not.toHaveBeenCalled()
expect(document.querySelector('.qr-placeholder')).not.toBeNull()
})
it('keeps the QR placeholder when QR generation fails', async () => {
toDataUrlMock.mockRejectedValueOnce(new Error('QR failed'))
+57 -56
View File
@@ -78,64 +78,61 @@ export function ShareCard({ transfer, onExtendTransfer }: Props) {
}, [qrShareUrl])
/** copyLink writes the full share URL, including the `#k=` fragment, to the clipboard. */
const copyLink = async () => {
if (!transfer?.shareUrl || !canShareLink) {
return
}
try {
setActionError(undefined)
await navigator.clipboard.writeText(transfer.shareUrl)
setCopyState('copied')
window.setTimeout(() => setCopyState('idle'), 1800)
} catch {
setCopyState('idle')
setActionError('Copy failed. Use your browser share tools or try again on this device.')
}
}
const copyLink = qrShareUrl
? async () => {
try {
setActionError(undefined)
await navigator.clipboard.writeText(qrShareUrl)
setCopyState('copied')
window.setTimeout(() => setCopyState('idle'), 1800)
} catch {
setCopyState('idle')
setActionError('Copy failed. Use your browser share tools or try again on this device.')
}
}
: undefined
/** shareLink uses the platform share sheet when available and falls back to copying. */
const shareLink = async () => {
if (!transfer?.shareUrl || !canShareLink) {
return
}
if (navigator.share) {
try {
setActionError(undefined)
await navigator.share({
title: transfer.displayName,
url: transfer.shareUrl,
text: 'Encrypted files via Xdrop',
})
return
} catch (error) {
if (isAbortLike(error)) {
return
const shareLink = qrShareUrl
? async () => {
if (navigator.share) {
try {
setActionError(undefined)
await navigator.share({
title: transfer?.displayName,
url: qrShareUrl,
text: 'Encrypted files via Xdrop',
})
return
} catch (error) {
if (isAbortLike(error)) {
return
}
await copyLink?.()
return
}
}
await copyLink()
return
await copyLink?.()
}
}
: undefined
await copyLink()
}
const extendExpiry = async () => {
if (!transfer || !canExtendTransfer || !onExtendTransfer) {
return
}
try {
setActionError(undefined)
setIsExtending(true)
await onExtendTransfer(transfer.id, MAX_EXPIRY_SECONDS)
} catch (error) {
setActionError(
error instanceof Error ? error.message : 'Could not update this transfer right now.',
)
} finally {
setIsExtending(false)
}
}
const extendExpiry =
canExtendTransfer && transfer && onExtendTransfer
? async () => {
try {
setActionError(undefined)
setIsExtending(true)
await onExtendTransfer(transfer.id, MAX_EXPIRY_SECONDS)
} catch (error) {
setActionError(
error instanceof Error ? error.message : 'Could not update this transfer right now.',
)
} finally {
setIsExtending(false)
}
}
: undefined
if (!transfer) {
return (
@@ -229,13 +226,17 @@ export function ShareCard({ transfer, onExtendTransfer }: Props) {
</dl>
<div className="button-row">
<Button onClick={() => void copyLink()} disabled={!canShareLink}>
<Button onClick={copyLink ? () => void copyLink() : undefined} disabled={!copyLink}>
{copyState === 'copied' ? 'Copied' : 'Copy link'}
</Button>
<Button onClick={() => void shareLink()} tone="ghost" disabled={!canShareLink}>
<Button
onClick={shareLink ? () => void shareLink() : undefined}
tone="ghost"
disabled={!shareLink}
>
Share link
</Button>
{canExtendTransfer ? (
{extendExpiry ? (
<Button tone="ghost" onClick={() => void extendExpiry()} disabled={isExtending}>
{isExtending
? 'Updating expiry…'
@@ -554,4 +554,41 @@ describe('TransferProvider actions', () => {
).toBeInTheDocument()
})
})
it('aborts active upload runtimes when deleting a transfer mid-flight', async () => {
transfersStore.set('t1', makeTransferRecord('uploading'))
sourcesStore.set('t1:file-1', makeSourceRecord('t1'))
let aborts = 0
vi.stubGlobal(
'fetch',
vi.fn(
(_url: string, init?: RequestInit) =>
new Promise((_, reject) => {
const signal = init?.signal as AbortSignal | undefined
signal?.addEventListener('abort', () => {
aborts += 1
reject(new DOMException('aborted', 'AbortError'))
})
}),
),
)
renderProvider()
await waitFor(() => {
expect(global.fetch).toHaveBeenCalled()
})
await waitFor(() => expect(latestContext).not.toBeNull())
await act(async () => {
await latestContext?.deleteTransfer('t1')
})
expect(aborts).toBeGreaterThan(0)
expect(deleteRemoteTransferMock).toHaveBeenCalledWith('t1', 'manage-token')
expect(deletePersistedTransferSourcesMock).toHaveBeenCalledWith('t1')
expect(deleteTransferRecordMock).toHaveBeenCalledWith('t1')
expect(transfersStore.has('t1')).toBe(false)
})
})
@@ -1,4 +1,4 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import {
DEFAULT_EXPIRY_SECONDS,
EXPIRY_OPTIONS,
@@ -355,6 +355,69 @@ describe('UploadStudio', () => {
).toBeInTheDocument()
})
it('avoids updating state when draft persistence resolves after unmount', async () => {
const persistedFile = new File(['hello'], 'draft.txt', { lastModified: 12, type: 'text/plain' })
let resolvePersist: ((sources: typeof draftState.sources) => void) | undefined
persistDraftMock.mockImplementationOnce(
() =>
new Promise((resolve) => {
resolvePersist = resolve
}),
)
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const { container, unmount } = renderStudio()
fireEvent.change(container.querySelector('input[type="file"]') as HTMLInputElement, {
target: { files: [persistedFile] },
})
unmount()
await act(async () => {
resolvePersist?.([
{
draftKey: 'draft-1',
file: persistedFile,
relativePath: 'draft.txt',
},
])
await Promise.resolve()
})
expect(errorSpy).not.toHaveBeenCalled()
errorSpy.mockRestore()
})
it('avoids surfacing persistence errors after unmount', async () => {
let rejectPersist: ((reason?: unknown) => void) | undefined
persistDraftMock.mockImplementationOnce(
() =>
new Promise((_, reject) => {
rejectPersist = reject
}),
)
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const { container, unmount } = renderStudio()
fireEvent.change(container.querySelector('input[type="file"]') as HTMLInputElement, {
target: {
files: [new File(['hello'], 'draft.txt', { lastModified: 12, type: 'text/plain' })],
},
})
unmount()
await act(async () => {
rejectPersist?.(new Error('persist failed'))
await Promise.resolve()
})
expect(errorSpy).not.toHaveBeenCalled()
errorSpy.mockRestore()
})
it('prefers webkitRelativePath over the bare filename when a folder is chosen', async () => {
const { container } = renderStudio()
const nestedFile = new File(['folder'], 'nested.txt', { lastModified: 14, type: 'text/plain' })
@@ -153,12 +153,6 @@ export function UploadStudio() {
/** launchTransfer hands the staged sources to the transfer runtime and navigates to share view. */
const launchTransfer = async () => {
if (selectionLimitError) {
setSelectionError(selectionLimitError)
setLaunchError(undefined)
return
}
try {
setIsLaunching(true)
setLaunchError(undefined)
+55 -1
View File
@@ -1,9 +1,18 @@
import { describe, expect, it } from 'vitest'
import { afterEach, describe, expect, it } from 'vitest'
import { sanitizePath } from '@/lib/files/paths'
import { buildBinaryZip, buildTextZip, buildZip, writeZipStream } from './zip'
describe('zip builder', () => {
const OriginalResponse = globalThis.Response
afterEach(() => {
Object.defineProperty(globalThis, 'Response', {
configurable: true,
value: OriginalResponse,
})
})
it('preserves sanitized folder structure', async () => {
const zipBlob = await buildZip([
{ path: '../folder/photo.png', blob: new Blob(['a']) },
@@ -101,4 +110,49 @@ describe('zip builder', () => {
).resolves.toBeUndefined()
expect(chunks.length).toBeGreaterThan(0)
})
it('uses arrayBuffer-backed blobs when stream() is unavailable', async () => {
const blobLike = {
arrayBuffer: async () => new TextEncoder().encode('hello').buffer,
size: 5,
type: 'text/plain',
} as unknown as Blob
const zipBlob = await buildZip([{ path: 'fallback/data.txt', blob: blobLike }])
expect(zipBlob.type).toBe('application/zip')
expect(zipBlob.size).toBeGreaterThan(0)
})
it('falls back to Response bodies when blob helpers are unavailable', async () => {
const blobLike = {
size: 5,
type: 'text/plain',
} as unknown as Blob
const zipBlob = await buildZip([{ path: 'fallback/data.txt', blob: blobLike }])
expect(zipBlob.type).toBe('application/zip')
expect(zipBlob.size).toBeGreaterThan(0)
})
it('throws when no blob streaming strategy is available', async () => {
class ResponseWithoutBody {
readonly body = null
}
Object.defineProperty(globalThis, 'Response', {
configurable: true,
value: ResponseWithoutBody,
})
const blobLike = {
size: 5,
type: 'text/plain',
} as unknown as Blob
await expect(buildZip([{ path: 'fallback/data.txt', blob: blobLike }])).rejects.toThrow(
'Blob stream is unavailable.',
)
})
})
@@ -137,6 +137,48 @@ describe('persistentSources', () => {
).resolves.toBeNull()
})
it('returns null when navigator is unavailable or the OPFS root cannot be opened', async () => {
const originalNavigator = globalThis.navigator
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: undefined,
})
await expect(
persistSourceToOpfs({
file: new File(['hello'], 'draft.txt'),
fileId: 'file-1',
relativePath: 'draft.txt',
transferId: 'transfer-1',
}),
).resolves.toBeNull()
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: originalNavigator,
})
const harness = createOpfsHarness()
harness.storage.getDirectory.mockResolvedValueOnce({
getDirectoryHandle: vi.fn(async () => {
throw new Error('denied')
}),
getFileHandle: vi.fn(),
removeEntry: vi.fn(async () => {}),
})
stubStorage(harness.storage)
await expect(
persistSourceToOpfs({
file: new File(['hello'], 'draft.txt'),
fileId: 'file-1',
relativePath: 'draft.txt',
transferId: 'transfer-1',
}),
).resolves.toBeNull()
})
it('restores indexeddb-backed files and handles missing files', async () => {
const file = new File(['payload'], 'example.txt', { type: 'text/plain', lastModified: 5 })
@@ -0,0 +1,76 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { formatLocalDateTime } from './formatDateTime'
const originalNavigator = globalThis.navigator
describe('formatLocalDateTime', () => {
afterEach(() => {
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: originalNavigator,
})
vi.restoreAllMocks()
})
it('returns an empty string for invalid date values', () => {
expect(formatLocalDateTime('not-a-date')).toBe('')
})
it('prefers navigator.language when navigator.languages is empty', () => {
let observedLocales: Intl.LocalesArgument | undefined
let observedOptions: Intl.DateTimeFormatOptions | undefined
const DateTimeFormatMock = vi
.spyOn(Intl, 'DateTimeFormat')
.mockImplementation(function (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) {
observedLocales = locales
observedOptions = options
return {
format: () => 'formatted',
} as Intl.DateTimeFormat
})
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
language: 'fr-FR',
languages: [],
},
})
expect(formatLocalDateTime('2026-03-20T10:00:00.000Z')).toBe('formatted')
expect(DateTimeFormatMock).toHaveBeenCalledTimes(1)
expect(observedLocales).toEqual(['fr-FR'])
expect(observedOptions).toMatchObject({
dateStyle: 'medium',
timeStyle: 'short',
})
})
it('falls back to an undefined locale list when navigator is unavailable', () => {
let observedLocales: Intl.LocalesArgument | undefined
let observedOptions: Intl.DateTimeFormatOptions | undefined
const DateTimeFormatMock = vi
.spyOn(Intl, 'DateTimeFormat')
.mockImplementation(function (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) {
observedLocales = locales
observedOptions = options
return {
format: () => 'formatted',
} as Intl.DateTimeFormat
})
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: undefined,
})
expect(formatLocalDateTime('2026-03-20T10:00:00.000Z')).toBe('formatted')
expect(DateTimeFormatMock).toHaveBeenCalledTimes(1)
expect(observedLocales).toBeUndefined()
expect(observedOptions).toMatchObject({
dateStyle: 'medium',
timeStyle: 'short',
})
})
})