mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
ae0d4e4520
Everything the server's UI does differently inside the iOS and Android app lives under src/native and is inert in a browser. Sign-in: SSO and the passkey button go through the system browser (signInThroughBrowser), because identity providers refuse embedded browsers and WebAuthn needs a domain association no build can have for every server. The browser end is the new /native-handoff page: it signs the user in if needed and hands the session over only after an explicit tap, so a link carrying somebody else's challenge cannot pass a session on silently. The verifier is hashed with @noble/hashes because crypto.subtle is missing on the plain-http servers many home networks use. An OIDC-only install no longer bounces to the IdP inside the WebView, and passkey management is hidden in the app. Downloads: the WebView cannot save a blob, so downloadBlob, downloadFile and openFile write to the app cache and open the share sheet. The eight places that built their own download anchor now use downloadBlob, which they should have anyway. The rest: "Change server" on the login page and in the account settings, the splash hides after the first frame, Android's back button walks history before it leaves the app, the status bar follows the scheme, and on iOS the start page is refreshed and every chunk of the build (listed in the new asset-manifest.json) is pulled into WebKit's cache so the app can start without network.
47 lines
2.1 KiB
TypeScript
47 lines
2.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { downloadBlob, downloadFile, openFile } from '../../../src/utils/fileDownload'
|
|
import { shareFile } from '../../../src/native/nativeFiles'
|
|
|
|
vi.mock('../../../src/db/offlineDb', () => ({ getCachedBlob: vi.fn() }))
|
|
vi.mock('../../../src/native/platform', () => ({ isNativeApp: () => true }))
|
|
vi.mock('../../../src/native/nativeFiles', async () => {
|
|
const actual = await vi.importActual<typeof import('../../../src/native/nativeFiles')>('../../../src/native/nativeFiles')
|
|
return { ...actual, shareFile: vi.fn() }
|
|
})
|
|
|
|
const pdf = new Blob(['%PDF'], { type: 'application/pdf' })
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(shareFile).mockReset().mockResolvedValue()
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, status: 200, blob: () => Promise.resolve(pdf) }))
|
|
})
|
|
|
|
describe('fileDownload inside the native app', () => {
|
|
it('hands an in-memory blob to the share sheet instead of an anchor', () => {
|
|
const click = vi.spyOn(HTMLAnchorElement.prototype, 'click')
|
|
downloadBlob(pdf, 'report.pdf')
|
|
expect(shareFile).toHaveBeenCalledWith(pdf, 'report.pdf')
|
|
expect(click).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('logs a share that failed rather than losing it', async () => {
|
|
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
vi.mocked(shareFile).mockRejectedValue(new Error('no space'))
|
|
downloadBlob(pdf, 'report.pdf')
|
|
await vi.waitFor(() => expect(error).toHaveBeenCalledWith('[download]', expect.any(Error)))
|
|
})
|
|
|
|
it('shares a protected file under its given name, or the one from its URL', async () => {
|
|
await downloadFile('/api/trips/1/files/2/download', 'Boarding pass.pdf')
|
|
expect(shareFile).toHaveBeenLastCalledWith(pdf, 'Boarding pass.pdf')
|
|
|
|
await downloadFile('/uploads/files/ticket%20A.pdf')
|
|
expect(shareFile).toHaveBeenLastCalledWith(pdf, 'ticket A.pdf')
|
|
})
|
|
|
|
it('opens files through the share sheet too, since there is no tab to open them in', async () => {
|
|
await openFile('/api/trips/1/files/2/download')
|
|
expect(shareFile).toHaveBeenLastCalledWith(pdf, 'download')
|
|
})
|
|
})
|