152 lines
6.6 KiB
TypeScript
152 lines
6.6 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { readFileSync } from "node:fs";
|
|
|
|
const workspaceSource = readFileSync(
|
|
new URL("../apps/mobile/src/screens/WorkspaceScreen.tsx", import.meta.url),
|
|
"utf8"
|
|
);
|
|
const workspaceEditorsSource = readFileSync(
|
|
new URL("../apps/mobile/src/screens/WorkspaceEditors.tsx", import.meta.url),
|
|
"utf8"
|
|
);
|
|
const memoDetailSource = readFileSync(
|
|
new URL("../apps/mobile/src/screens/WorkspaceMemoDetail.tsx", import.meta.url),
|
|
"utf8"
|
|
);
|
|
const localTiptapEditorSource = readFileSync(
|
|
new URL("../apps/mobile/src/components/LocalTiptapEditor.tsx", import.meta.url),
|
|
"utf8"
|
|
);
|
|
const notesViewSource = readFileSync(
|
|
new URL("../apps/mobile/src/screens/WorkspaceNotesView.tsx", import.meta.url),
|
|
"utf8"
|
|
);
|
|
const mobileDomSource = readFileSync(
|
|
new URL("../apps/mobile/src/lib/mobile-dom.ts", import.meta.url),
|
|
"utf8"
|
|
);
|
|
const appJson = JSON.parse(
|
|
readFileSync(new URL("../apps/mobile/app.json", import.meta.url), "utf8")
|
|
) as {
|
|
expo: {
|
|
ios?: {
|
|
infoPlist?: Record<string, unknown>;
|
|
supportsTablet?: boolean;
|
|
};
|
|
};
|
|
};
|
|
const accountSecuritySource = readFileSync(
|
|
new URL("../apps/mobile/src/screens/AccountSecurityModal.tsx", import.meta.url),
|
|
"utf8"
|
|
);
|
|
|
|
describe("mobile app scope", () => {
|
|
test("keeps workspace administration out of the native app", () => {
|
|
for (const removedCapability of [
|
|
"ApiTokensModal",
|
|
"ResourcesModal",
|
|
"TagsManagerModal",
|
|
"createApiToken",
|
|
"deleteApiToken",
|
|
"mergeMemos",
|
|
]) {
|
|
expect(workspaceSource).not.toContain(removedCapability);
|
|
}
|
|
});
|
|
|
|
test("does not initialize a hidden WebView during workspace startup", () => {
|
|
expect(workspaceSource).not.toContain("EditorRuntimePrewarm");
|
|
expect(workspaceSource).not.toContain("editorRuntimeWarm");
|
|
});
|
|
|
|
test("limits account security to the signed-in user", () => {
|
|
for (const removedCapability of ["createUser", "listUsers", "updateUser"]) {
|
|
expect(accountSecuritySource).not.toContain(removedCapability);
|
|
}
|
|
});
|
|
|
|
test("keeps version history reachable from an active note", () => {
|
|
expect(memoDetailSource).toMatch(
|
|
/\{memo && !memo\.isDeleted \? \(\s*<Pressable\s+accessibilityLabel="版本历史"/
|
|
);
|
|
expect(memoDetailSource).toContain('syncStatus === "conflict"');
|
|
expect(memoDetailSource).toContain("onResolveSyncConflict");
|
|
});
|
|
|
|
test("renders note detail body with the shared read-only TipTap viewer", () => {
|
|
expect(memoDetailSource).toContain('mode="viewer"');
|
|
expect(memoDetailSource).toContain("LocalTiptapEditor");
|
|
expect(memoDetailSource).not.toContain("react-native-markdown-display");
|
|
});
|
|
|
|
test("carries workspace search into note detail and scrolls active matches", () => {
|
|
expect(workspaceSource).toContain('initialSearchQuery={selectedMemoId ? searchText.trim() : ""}');
|
|
expect(memoDetailSource).toContain("metadataSearchMatchCount + bodySearchMatchCount");
|
|
expect(memoDetailSource).toContain("const retryTimers = [120, 360]");
|
|
expect(localTiptapEditorSource).toContain("createMobileNoteSearchHighlightPlugin");
|
|
expect(localTiptapEditorSource).toContain("scrollEditorPositionIntoView(editor, match.from");
|
|
});
|
|
|
|
test("keeps the Android editor caret visible while the keyboard viewport changes", () => {
|
|
expect(workspaceEditorsSource).toContain("KeyboardAvoidingView");
|
|
expect(workspaceEditorsSource).toContain('enabled={Platform.OS === "android"}');
|
|
expect(localTiptapEditorSource).toContain('visualViewport?.addEventListener("resize", ensureSelectionVisible)');
|
|
expect(localTiptapEditorSource).toContain("--edgeever-keyboard-inset");
|
|
expect(localTiptapEditorSource).toContain("scrollEditorPositionIntoView(editor, editor.state.selection.head)");
|
|
});
|
|
|
|
test("keeps Android memo list motion and spring feedback", () => {
|
|
expect(notesViewSource).toContain("FadeInDown.duration(260).springify().damping(18)");
|
|
expect(notesViewSource).toContain("FadeOutUp.duration(220)");
|
|
expect(notesViewSource).toContain("LinearTransition.duration(220)");
|
|
expect(notesViewSource).toContain("pressScale.value = withTiming(0.985");
|
|
expect(notesViewSource).toContain("pressScale.value = withTiming(1");
|
|
});
|
|
|
|
test("hardens DOM/WebView hosts against media capture probes during App Review", () => {
|
|
expect(mobileDomSource).toContain('mediaCapturePermissionGrantType: "deny"');
|
|
expect(mobileDomSource).toContain("mediaPlaybackRequiresUserAction: true");
|
|
expect(workspaceEditorsSource).toContain("SAFE_DOM_WEBVIEW_PROPS");
|
|
expect(memoDetailSource).toContain("SAFE_DOM_WEBVIEW_PROPS");
|
|
});
|
|
|
|
test("reads the latest create and upload state from the hardware-back handler", () => {
|
|
expect(workspaceEditorsSource).toContain("createPendingRef.current || imageOperationRef.current");
|
|
});
|
|
|
|
test("focuses the note body instead of the title when creating a note", () => {
|
|
const createMemoSource = workspaceEditorsSource.slice(
|
|
workspaceEditorsSource.indexOf("export const CreateMemoModal ="),
|
|
workspaceEditorsSource.indexOf("export const RichEditorModal =")
|
|
);
|
|
const titleInput = createMemoSource.match(
|
|
/<TextInput\s+autoCorrect\s+accessibilityLabel="笔记标题"[\s\S]*?\/>/
|
|
)?.[0];
|
|
|
|
expect(createMemoSource).toMatch(/<LocalTiptapEditor\s+autoFocus\s/);
|
|
expect(createMemoSource).toContain("scheduleBodyKeyboard(180, false)");
|
|
expect(titleInput).toBeDefined();
|
|
expect(titleInput).not.toContain("autoFocus");
|
|
expect(createMemoSource).not.toContain("scheduleTitleFocus");
|
|
});
|
|
|
|
test("keeps editor startup recoverable and avoids competing autofocus paths", () => {
|
|
expect(workspaceEditorsSource).toContain("MOBILE_EDITOR_STARTUP_TIMEOUT_MS");
|
|
expect(workspaceEditorsSource).toContain("MobileEditorStartupOverlay");
|
|
expect(workspaceEditorsSource).toContain("key={editorStartup.attempt}");
|
|
expect(localTiptapEditorSource).toContain("autofocus: false");
|
|
expect(localTiptapEditorSource).toContain('import("mermaid/dist/mermaid.min.js")');
|
|
expect(localTiptapEditorSource).toContain('import("beautiful-mermaid")');
|
|
expect(localTiptapEditorSource).toContain('import("html-to-image")');
|
|
expect(localTiptapEditorSource).not.toContain('import "mermaid/dist/mermaid.min.js"');
|
|
});
|
|
|
|
test("declares iOS privacy strings and full-screen phone-on-iPad presentation", () => {
|
|
const infoPlist = appJson.expo.ios?.infoPlist ?? {};
|
|
expect(appJson.expo.ios?.supportsTablet).toBe(false);
|
|
expect(infoPlist.UIRequiresFullScreen).toBe(true);
|
|
expect(String(infoPlist.NSMicrophoneUsageDescription ?? "")).toMatch(/microphone/i);
|
|
expect(String(infoPlist.NSCameraUsageDescription ?? "")).toMatch(/camera/i);
|
|
});
|
|
});
|