mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
fix(mobile): stabilize localized error and key handling
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
type AccountsSnapshot,
|
||||
type ProviderKey,
|
||||
decodeAccountsSnapshot,
|
||||
isInvalidAccountsSnapshotError,
|
||||
getActiveProviderRateLimits,
|
||||
getInactiveProviderUsage,
|
||||
getUsageBarState,
|
||||
@@ -142,7 +143,7 @@ export default function AccountsScreen() {
|
||||
setError(res.error.message)
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message === 'Invalid accounts snapshot from host') {
|
||||
if (isInvalidAccountsSnapshotError(e)) {
|
||||
rejectInvalidSnapshot()
|
||||
} else {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
|
||||
@@ -15,6 +15,7 @@ export type {
|
||||
} from './account-usage-state'
|
||||
export {
|
||||
decodeAccountsSnapshot,
|
||||
isInvalidAccountsSnapshotError,
|
||||
getActiveProviderRateLimits,
|
||||
getInactiveProviderUsage,
|
||||
getUsageBarState,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
buildTerminalShortcutKey,
|
||||
normalizeShortcutKeyInput,
|
||||
TERMINAL_SHORTCUT_SPECIAL_KEYS,
|
||||
TERMINAL_SHORTCUT_MODIFIER_LABELS,
|
||||
type TerminalShortcutModifier,
|
||||
type TerminalShortcutSpecialKey
|
||||
} from '../terminal/terminal-accessory-keys'
|
||||
@@ -60,9 +61,9 @@ export function CustomKeyModal({ visible, onClose, onKeysChanged, onManageShortc
|
||||
const [macroEnter, setMacroEnter] = useState(true)
|
||||
const [previousVisible, setPreviousVisible] = useState(visible)
|
||||
const shortcutModifierOptions = [
|
||||
{ id: 'ctrl', label: t('m.OZLCEBo') },
|
||||
{ id: 'alt', label: t('m.Mz-qIO4'), glyph: '⌥' },
|
||||
{ id: 'shift', label: t('m.Dl9mAnI') }
|
||||
{ id: 'ctrl', label: TERMINAL_SHORTCUT_MODIFIER_LABELS.ctrl },
|
||||
{ id: 'alt', label: TERMINAL_SHORTCUT_MODIFIER_LABELS.alt, glyph: '⌥' },
|
||||
{ id: 'shift', label: TERMINAL_SHORTCUT_MODIFIER_LABELS.shift }
|
||||
] satisfies readonly { id: TerminalShortcutModifier; label: string; glyph?: string }[]
|
||||
const specialKeyGroups: { title: string; ids: string[]; columns: number }[] = [
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
export {
|
||||
AccountsSnapshotSchema,
|
||||
decodeAccountsSnapshot,
|
||||
isInvalidAccountsSnapshotError,
|
||||
ProviderRateLimitsSchema,
|
||||
RateLimitRuntimeTargetSchema,
|
||||
type AccountsSnapshot,
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { decodeAccountsSnapshot } from './accounts-snapshot'
|
||||
import { mobileI18n } from '../i18n/mobile-i18n'
|
||||
import { decodeAccountsSnapshot, isInvalidAccountsSnapshotError } from './accounts-snapshot'
|
||||
|
||||
const INITIAL_LOCALE = mobileI18n.language
|
||||
|
||||
afterEach(async () => {
|
||||
await mobileI18n.changeLanguage(INITIAL_LOCALE)
|
||||
})
|
||||
|
||||
function makeSnapshot(): unknown {
|
||||
return {
|
||||
@@ -81,6 +88,21 @@ function setPath(root: unknown, path: string[], value: unknown): void {
|
||||
}
|
||||
|
||||
describe('decodeAccountsSnapshot', () => {
|
||||
it('classifies an invalid snapshot independently of its localized message', async () => {
|
||||
await mobileI18n.changeLanguage('es')
|
||||
|
||||
let thrown: unknown
|
||||
try {
|
||||
decodeAccountsSnapshot(null)
|
||||
} catch (error) {
|
||||
thrown = error
|
||||
}
|
||||
|
||||
expect(isInvalidAccountsSnapshotError(thrown)).toBe(true)
|
||||
expect(thrown).toBeInstanceOf(Error)
|
||||
expect((thrown as Error).message).not.toBe('Invalid accounts snapshot from host')
|
||||
})
|
||||
|
||||
it('validates nested account/rate-limit state and preserves forward-compatible fields', () => {
|
||||
const snapshot = decodeAccountsSnapshot(makeSnapshot())
|
||||
|
||||
|
||||
@@ -228,10 +228,21 @@ export type ClaudeAccountSummary = z.infer<typeof ClaudeAccountSummarySchema>
|
||||
export type CodexAccountSummary = z.infer<typeof CodexAccountSummarySchema>
|
||||
export type AccountsSnapshot = z.infer<typeof AccountsSnapshotSchema>
|
||||
|
||||
export class InvalidAccountsSnapshotError extends Error {
|
||||
constructor() {
|
||||
super(t('m.l0xDyns'))
|
||||
this.name = 'InvalidAccountsSnapshotError'
|
||||
}
|
||||
}
|
||||
|
||||
export function isInvalidAccountsSnapshotError(error: unknown): boolean {
|
||||
return error instanceof InvalidAccountsSnapshotError
|
||||
}
|
||||
|
||||
export function decodeAccountsSnapshot(value: unknown): AccountsSnapshot {
|
||||
const result = AccountsSnapshotSchema.safeParse(value)
|
||||
if (!result.success) {
|
||||
throw new Error(t('m.l0xDyns'))
|
||||
throw new InvalidAccountsSnapshotError()
|
||||
}
|
||||
return result.data
|
||||
}
|
||||
|
||||
@@ -71,6 +71,25 @@ describe('mobile i18n', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps generated terminal shortcut labels canonical in translated locales', async () => {
|
||||
await mobileI18n.changeLanguage('zh')
|
||||
const {
|
||||
buildTerminalShortcutKey,
|
||||
TERMINAL_SHORTCUT_MODIFIER_LABELS,
|
||||
TERMINAL_SHORTCUT_SPECIAL_KEYS
|
||||
} = await import('../terminal/terminal-accessory-keys')
|
||||
|
||||
expect(TERMINAL_SHORTCUT_MODIFIER_LABELS).toEqual({
|
||||
ctrl: 'Ctrl',
|
||||
alt: 'Alt',
|
||||
shift: 'Shift'
|
||||
})
|
||||
expect(TERMINAL_SHORTCUT_SPECIAL_KEYS.find((key) => key.id === 'pageDown')?.label).toBe('PgDn')
|
||||
expect(buildTerminalShortcutKey({ key: 'pageDown', modifiers: ['shift'] })?.label).toBe(
|
||||
'Shift+PgDn'
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['es', ['Sin comprobaciones', 'En staging', 'Nota de revisión']],
|
||||
['ja', ['チェックなし', 'ステージ済み', 'レビューメモ']],
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client'
|
||||
import type { RpcFailure, RpcResponse, RpcSuccess } from '../transport/types'
|
||||
import { isClipboardImageTooLargeError } from '../../../src/shared/clipboard-image'
|
||||
import { mobileI18n } from '../i18n/mobile-i18n'
|
||||
|
||||
function ok(id: string, result: unknown): RpcSuccess {
|
||||
return { id, ok: true, result, _meta: { runtimeId: 'runtime-1' } }
|
||||
@@ -37,6 +39,21 @@ function clientWithResponses(responses: RpcResponse[]): Pick<RpcClient, 'sendReq
|
||||
}
|
||||
|
||||
describe('mobile clipboard image paste helpers', () => {
|
||||
it('keeps the oversized-image classifier stable in a translated locale', async () => {
|
||||
const initialLocale = mobileI18n.language
|
||||
await mobileI18n.changeLanguage('es')
|
||||
let thrown: unknown
|
||||
try {
|
||||
normalizeMobileClipboardImageBase64('a'.repeat(24 * 1024 * 1024 + 1))
|
||||
} catch (error) {
|
||||
thrown = error
|
||||
} finally {
|
||||
await mobileI18n.changeLanguage(initialLocale)
|
||||
}
|
||||
|
||||
expect(isClipboardImageTooLargeError(thrown)).toBe(true)
|
||||
})
|
||||
|
||||
it('strips data URL image prefixes', () => {
|
||||
expect(normalizeMobileClipboardImageBase64('data:image/png;base64,aGVsbG8=')).toBe('aGVsbG8=')
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { isLogicalClientCutoverError } from '../transport/stable-logical-rpc-client'
|
||||
import type { RpcFailure, RpcSuccess } from '../transport/types'
|
||||
import { CLIPBOARD_IMAGE_TOO_LARGE_ERROR } from '../../../src/shared/clipboard-image'
|
||||
import { t } from '@/i18n/mobile-i18n'
|
||||
|
||||
export const MOBILE_CLIPBOARD_IMAGE_MAX_BASE64_CHARS = 24 * 1024 * 1024
|
||||
@@ -18,7 +19,7 @@ const BASE64_PATTERN = /^[A-Za-z0-9+/]*={0,2}$/
|
||||
export function normalizeMobileClipboardImageBase64(data: string): string {
|
||||
const contentBase64 = data.replace(DATA_URL_PREFIX_RE, '')
|
||||
if (contentBase64.length > MOBILE_CLIPBOARD_IMAGE_MAX_BASE64_CHARS) {
|
||||
throw new Error(t('m.Oht8bkc'))
|
||||
throw new Error(CLIPBOARD_IMAGE_TOO_LARGE_ERROR)
|
||||
}
|
||||
if (contentBase64.length % 4 === 1 || !BASE64_PATTERN.test(contentBase64)) {
|
||||
throw new Error(t('m.64b63WI'))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import type { ConnectionState } from '../transport/types'
|
||||
import { isClipboardImageTooLargeError } from '../../../src/shared/clipboard-image'
|
||||
import { attachMobileImageToTerminal } from './mobile-image-attachment'
|
||||
import {
|
||||
ImageLibraryPermissionError,
|
||||
@@ -35,10 +36,6 @@ type MobileImageAttachment = {
|
||||
readonly isAttaching: boolean
|
||||
}
|
||||
|
||||
function getErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
export function useMobileImageAttachment({
|
||||
client,
|
||||
activeHandle,
|
||||
@@ -81,7 +78,7 @@ export function useMobileImageAttachment({
|
||||
showToast(t('m.JdGd1Ic'), 1500)
|
||||
return
|
||||
}
|
||||
if (getErrorMessage(error) === 'Clipboard image is too large') {
|
||||
if (isClipboardImageTooLargeError(error)) {
|
||||
showToast(t('m.hJYA7AQ'), 1500)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { CLIPBOARD_IMAGE_TOO_LARGE_ERROR } from '../../../src/shared/clipboard-image'
|
||||
import { isClipboardImageTooLargeError } from '../../../src/shared/clipboard-image'
|
||||
import { buildAgentTuiClearInputForText } from '../../../src/shared/agent-tui-input-clear'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import type { ConnectionState } from '../transport/types'
|
||||
@@ -79,10 +79,6 @@ export type MobileNativeChatImageAttachments = {
|
||||
readonly sendNativeChat: (text: string) => Promise<boolean>
|
||||
}
|
||||
|
||||
function getErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
const NO_ATTACHMENTS: PendingNativeChatImage[] = []
|
||||
|
||||
function withScopeAttachments(
|
||||
@@ -174,7 +170,7 @@ export function useMobileNativeChatImageAttachments({
|
||||
showToast(t('m.jdkmIaw'), 1500)
|
||||
return
|
||||
}
|
||||
if (getErrorMessage(error) === CLIPBOARD_IMAGE_TOO_LARGE_ERROR) {
|
||||
if (isClipboardImageTooLargeError(error)) {
|
||||
showToast(t('m.-YjNIcU'), 1500)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ImageManipulator, SaveFormat } from 'expo-image-manipulator'
|
||||
import type { TerminalModes } from '../terminal/terminal-webview-contract'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import type { ConnectionState } from '../transport/types'
|
||||
import { isClipboardImageTooLargeError } from '../../../src/shared/clipboard-image'
|
||||
import {
|
||||
buildMobileImagePastePayload,
|
||||
prepareMobileClipboardImageBase64,
|
||||
@@ -174,7 +175,7 @@ export function useMobileTerminalPaste({
|
||||
console.warn('[mobile-clip] paste failed', { name: err.name, message: err.message })
|
||||
if (isDisconnected) {
|
||||
showToast(t('m.gG0saX0'), 1500)
|
||||
} else if (err.message === 'Clipboard image is too large') {
|
||||
} else if (isClipboardImageTooLargeError(e)) {
|
||||
showToast(t('m.5dhlsXo'), 1500)
|
||||
} else {
|
||||
showToast(t('m.weTQMew'), 1500)
|
||||
|
||||
@@ -2,12 +2,12 @@ import { t } from '@/i18n/mobile-i18n'
|
||||
import type { TerminalAccessoryKey } from './terminal-accessory-keys'
|
||||
|
||||
export const TERMINAL_ACCESSORY_KEYS: TerminalAccessoryKey[] = [
|
||||
{ id: 'escape', label: t('m.bNejRmQ'), bytes: '\x1b', accessibilityLabel: t('m.jHe6UUU') },
|
||||
{ id: 'tab', label: t('m.hM5KUAw'), bytes: '\t', accessibilityLabel: t('m.hM5KUAw') },
|
||||
{ id: 'enter', label: t('m.9KFo5zM'), bytes: '\r', accessibilityLabel: t('m.9KFo5zM') },
|
||||
{ id: 'escape', label: 'Esc', bytes: '\x1b', accessibilityLabel: t('m.jHe6UUU') },
|
||||
{ id: 'tab', label: 'Tab', bytes: '\t', accessibilityLabel: t('m.hM5KUAw') },
|
||||
{ id: 'enter', label: 'Enter', bytes: '\r', accessibilityLabel: t('m.9KFo5zM') },
|
||||
// Why: terminal apps recognize ESC [ Z as the reverse-tab sequence.
|
||||
{ id: 'shiftTab', label: t('m.S9DH8y4'), bytes: '\x1b[Z', accessibilityLabel: t('m.p7_d2FU') },
|
||||
{ id: 'space', label: t('m.l6KJT3M'), bytes: ' ', accessibilityLabel: t('m.l6KJT3M') },
|
||||
{ id: 'shiftTab', label: 'Shift+Tab', bytes: '\x1b[Z', accessibilityLabel: t('m.p7_d2FU') },
|
||||
{ id: 'space', label: 'Space', bytes: ' ', accessibilityLabel: t('m.l6KJT3M') },
|
||||
{
|
||||
id: 'backspace',
|
||||
label: '⌫',
|
||||
@@ -17,7 +17,7 @@ export const TERMINAL_ACCESSORY_KEYS: TerminalAccessoryKey[] = [
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
label: t('m.gmy3A7M'),
|
||||
label: 'Del',
|
||||
bytes: '\x1b[3~',
|
||||
accessibilityLabel: t('m.fNgDk9o'),
|
||||
repeatable: true
|
||||
@@ -50,13 +50,13 @@ export const TERMINAL_ACCESSORY_KEYS: TerminalAccessoryKey[] = [
|
||||
accessibilityLabel: t('m.v5WSGO0'),
|
||||
repeatable: true
|
||||
},
|
||||
{ id: 'ctrlC', label: t('m.ke03J5M'), bytes: '\x03', accessibilityLabel: t('m.AzHoQzM') },
|
||||
{ id: 'ctrlD', label: t('m.vFYSo0s'), bytes: '\x04', accessibilityLabel: t('m.PBPnPeQ') },
|
||||
{ id: 'ctrlL', label: t('m.R6ZcIz8'), bytes: '\x0c', accessibilityLabel: t('m.Eqaj8oY') },
|
||||
{ id: 'ctrlZ', label: t('m.NIdsbYo'), bytes: '\x1a', accessibilityLabel: t('m.hqWfNmA') },
|
||||
{ id: 'ctrlR', label: t('m.a2Cb3GE'), bytes: '\x12', accessibilityLabel: t('m.mcvHf4o') },
|
||||
{ id: 'ctrlA', label: t('m.u9q9lPQ'), bytes: '\x01', accessibilityLabel: t('m.CKFp_QM') },
|
||||
{ id: 'ctrlE', label: t('m.5m_ucwE'), bytes: '\x05', accessibilityLabel: t('m.0VWtVO8') },
|
||||
{ id: 'ctrlW', label: t('m.jVVj75Y'), bytes: '\x17', accessibilityLabel: t('m.Bd9qfdk') },
|
||||
{ id: 'ctrlU', label: t('m.sL5fKGQ'), bytes: '\x15', accessibilityLabel: t('m.HrWsdDM') }
|
||||
{ id: 'ctrlC', label: 'Ctrl+C', bytes: '\x03', accessibilityLabel: t('m.AzHoQzM') },
|
||||
{ id: 'ctrlD', label: 'Ctrl+D', bytes: '\x04', accessibilityLabel: t('m.PBPnPeQ') },
|
||||
{ id: 'ctrlL', label: 'Ctrl+L', bytes: '\x0c', accessibilityLabel: t('m.Eqaj8oY') },
|
||||
{ id: 'ctrlZ', label: 'Ctrl+Z', bytes: '\x1a', accessibilityLabel: t('m.hqWfNmA') },
|
||||
{ id: 'ctrlR', label: 'Ctrl+R', bytes: '\x12', accessibilityLabel: t('m.mcvHf4o') },
|
||||
{ id: 'ctrlA', label: 'Ctrl+A', bytes: '\x01', accessibilityLabel: t('m.CKFp_QM') },
|
||||
{ id: 'ctrlE', label: 'Ctrl+E', bytes: '\x05', accessibilityLabel: t('m.0VWtVO8') },
|
||||
{ id: 'ctrlW', label: 'Ctrl+W', bytes: '\x17', accessibilityLabel: t('m.Bd9qfdk') },
|
||||
{ id: 'ctrlU', label: 'Ctrl+U', bytes: '\x15', accessibilityLabel: t('m.HrWsdDM') }
|
||||
]
|
||||
|
||||
@@ -29,11 +29,12 @@ export type TerminalShortcutSpecialKey = {
|
||||
|
||||
const ESC = '\x1b'
|
||||
|
||||
const MODIFIER_LABELS: Record<TerminalShortcutModifier, string> = {
|
||||
ctrl: t('m.mthhwXw'),
|
||||
alt: t('m.zN_6g9g'),
|
||||
shift: t('m.lCZrWn4')
|
||||
}
|
||||
export const TERMINAL_SHORTCUT_MODIFIER_LABELS: Readonly<Record<TerminalShortcutModifier, string>> =
|
||||
{
|
||||
ctrl: 'Ctrl',
|
||||
alt: 'Alt',
|
||||
shift: 'Shift'
|
||||
}
|
||||
|
||||
const MODIFIER_ORDER: TerminalShortcutModifier[] = ['ctrl', 'alt', 'shift']
|
||||
|
||||
@@ -78,33 +79,33 @@ const CTRL_PRINTABLE_BYTES: Record<string, string> = {
|
||||
}
|
||||
|
||||
const SPECIAL_KEY_LABELS: Record<string, string> = {
|
||||
escape: t('m.bNejRmQ'),
|
||||
tab: t('m.hM5KUAw'),
|
||||
enter: t('m.9KFo5zM'),
|
||||
escape: 'Esc',
|
||||
tab: 'Tab',
|
||||
enter: 'Enter',
|
||||
backspace: '⌫',
|
||||
delete: t('m.gmy3A7M'),
|
||||
insert: t('m.bh9E9Gk'),
|
||||
delete: 'Del',
|
||||
insert: 'Ins',
|
||||
arrowUp: '↑',
|
||||
arrowDown: '↓',
|
||||
arrowLeft: '←',
|
||||
arrowRight: '→',
|
||||
home: t('m.r2L-IvQ'),
|
||||
end: t('m.6UajMWQ'),
|
||||
pageUp: t('m.1qkhXPI'),
|
||||
pageDown: t('m.Wt9qvqo'),
|
||||
space: t('m.l6KJT3M'),
|
||||
f1: t('m.wPNSaMQ'),
|
||||
f2: t('m.5OQeDMc'),
|
||||
f3: t('m.fNbslzw'),
|
||||
f4: t('m.dMpOhHI'),
|
||||
f5: t('m.MmdqLFU'),
|
||||
f6: t('m.xOj4ngY'),
|
||||
f7: t('m.EbNp-x4'),
|
||||
f8: t('m.DR9i7SU'),
|
||||
f9: t('m.rI3MEx0'),
|
||||
f10: t('m.d6orzBg'),
|
||||
f11: t('m.T7RTOC4'),
|
||||
f12: t('m._dDm4ic')
|
||||
home: 'Home',
|
||||
end: 'End',
|
||||
pageUp: 'PgUp',
|
||||
pageDown: 'PgDn',
|
||||
space: 'Space',
|
||||
f1: 'F1',
|
||||
f2: 'F2',
|
||||
f3: 'F3',
|
||||
f4: 'F4',
|
||||
f5: 'F5',
|
||||
f6: 'F6',
|
||||
f7: 'F7',
|
||||
f8: 'F8',
|
||||
f9: 'F9',
|
||||
f10: 'F10',
|
||||
f11: 'F11',
|
||||
f12: 'F12'
|
||||
}
|
||||
|
||||
const SPECIAL_KEY_ACCESSIBILITY_LABELS: Record<string, string> = {
|
||||
@@ -350,7 +351,7 @@ function isPrintableShortcutKey(key: string): boolean {
|
||||
}
|
||||
|
||||
function formatShortcutLabel(key: string, modifiers: TerminalShortcutModifier[]): string {
|
||||
const modifierLabels = modifiers.map((modifier) => MODIFIER_LABELS[modifier])
|
||||
const modifierLabels = modifiers.map((modifier) => TERMINAL_SHORTCUT_MODIFIER_LABELS[modifier])
|
||||
return [...modifierLabels, displayKeyLabel(key)].join('+')
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
CLIPBOARD_IMAGE_MAX_SOURCE_BYTES,
|
||||
assertClipboardImageBase64LengthWithinLimit,
|
||||
assertClipboardImageByteLengthWithinLimit,
|
||||
assertClipboardImageDimensionsWithinLimit
|
||||
assertClipboardImageDimensionsWithinLimit,
|
||||
isClipboardImageTooLargeError
|
||||
} from './clipboard-image'
|
||||
|
||||
describe('clipboard image limits', () => {
|
||||
@@ -28,6 +29,11 @@ describe('clipboard image limits', () => {
|
||||
).toThrow('Clipboard image is too large')
|
||||
})
|
||||
|
||||
it('classifies only the stable internal oversized-image sentinel', () => {
|
||||
expect(isClipboardImageTooLargeError(new Error('Clipboard image is too large'))).toBe(true)
|
||||
expect(isClipboardImageTooLargeError(new Error('La imagen es demasiado grande'))).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects invalid or oversized image dimensions before pixel allocation', () => {
|
||||
expect(() =>
|
||||
assertClipboardImageDimensionsWithinLimit({
|
||||
|
||||
@@ -5,6 +5,10 @@ export const CLIPBOARD_IMAGE_MAX_SOURCE_BYTES = Math.floor(
|
||||
export const CLIPBOARD_IMAGE_MAX_PIXELS = 32 * 1024 * 1024
|
||||
export const CLIPBOARD_IMAGE_TOO_LARGE_ERROR = 'Clipboard image is too large'
|
||||
|
||||
export function isClipboardImageTooLargeError(error: unknown): boolean {
|
||||
return error instanceof Error && error.message === CLIPBOARD_IMAGE_TOO_LARGE_ERROR
|
||||
}
|
||||
|
||||
export type ClipboardImageDimensions = {
|
||||
height: number
|
||||
width: number
|
||||
|
||||
@@ -28,14 +28,16 @@ import {
|
||||
stopDevApp,
|
||||
waitForStoreReady
|
||||
} from '../../config/scripts/windows-apphang-repro/electron-dev-session.mjs'
|
||||
import { createCompletedOnboardingProfile } from '../../config/scripts/windows-apphang-repro/wsl-workspace-fixture.mjs'
|
||||
import {
|
||||
createCompletedOnboardingProfile,
|
||||
safeRemoveLocalDirectory
|
||||
} from '../../config/scripts/windows-apphang-repro/wsl-workspace-fixture.mjs'
|
||||
import {
|
||||
pollUntil,
|
||||
rendererActionTimeoutMs,
|
||||
runWithTimeout,
|
||||
setupTimeoutMs
|
||||
} from '../../config/scripts/windows-apphang-repro/repro-timing.mjs'
|
||||
import { safeRemoveLocalDirectory } from '../../config/scripts/windows-apphang-repro/wsl-workspace-fixture.mjs'
|
||||
|
||||
const rootDir = path.resolve(fileURLToPath(new URL('../..', import.meta.url)))
|
||||
const PARK_DELAY_MS = 1_500
|
||||
|
||||
@@ -34,14 +34,16 @@ import {
|
||||
stopDevApp,
|
||||
waitForStoreReady
|
||||
} from '../../config/scripts/windows-apphang-repro/electron-dev-session.mjs'
|
||||
import { createCompletedOnboardingProfile } from '../../config/scripts/windows-apphang-repro/wsl-workspace-fixture.mjs'
|
||||
import {
|
||||
createCompletedOnboardingProfile,
|
||||
safeRemoveLocalDirectory
|
||||
} from '../../config/scripts/windows-apphang-repro/wsl-workspace-fixture.mjs'
|
||||
import {
|
||||
pollUntil,
|
||||
rendererActionTimeoutMs,
|
||||
runWithTimeout,
|
||||
setupTimeoutMs
|
||||
} from '../../config/scripts/windows-apphang-repro/repro-timing.mjs'
|
||||
import { safeRemoveLocalDirectory } from '../../config/scripts/windows-apphang-repro/wsl-workspace-fixture.mjs'
|
||||
|
||||
const rootDir = path.resolve(fileURLToPath(new URL('../..', import.meta.url)))
|
||||
const scenarioTimeoutMs = 300_000
|
||||
|
||||
Reference in New Issue
Block a user