mindoodb-app-sdk 0.0.54 → 0.0.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  MINDOODB_APP_VALUE_TAG,
3
+ pageUsers,
3
4
  releaseMindooDBAppBridgeSessions
4
- } from "../chunk-6NTVTL6G.js";
5
+ } from "../chunk-S73KTYZF.js";
5
6
 
6
7
  // src/testing/index.ts
7
8
  import * as Automerge from "@automerge/automerge";
@@ -454,7 +455,202 @@ async function createEvaluatingViewNavigator(input, documents) {
454
455
  return wrapNavigator(input.definition, navigator);
455
456
  }
456
457
 
458
+ // src/testing/capabilityGuard.ts
459
+ var NONE = [];
460
+ var READ = ["read"];
461
+ var DOCUMENT_REQUIREMENTS = {
462
+ // capability probes answer false instead of failing
463
+ canChange: NONE,
464
+ canCreate: NONE,
465
+ canDelete: NONE,
466
+ canUndelete: NONE,
467
+ create: ["create"],
468
+ createMany: ["create"],
469
+ getDefaultCreateKeyId: ["create"],
470
+ listCreateKeys: ["create"],
471
+ update: ["update"],
472
+ applyAutomergeChanges: ["update"],
473
+ applyAutomergeChangesBatch: ["update"],
474
+ addRecipients: ["update"],
475
+ removeRecipients: ["update"],
476
+ setRecipients: ["update"],
477
+ delete: ["delete"],
478
+ deleteMany: ["delete"],
479
+ undelete: ["delete"],
480
+ listHistory: ["history"],
481
+ getAtHeads: ["history"],
482
+ getAtRevision: ["history"],
483
+ getAtTimestamp: ["history"],
484
+ listVerification: ["history"]
485
+ };
486
+ var ATTACHMENT_REQUIREMENTS = {
487
+ openWriteStream: ["attachments", "update"],
488
+ remove: ["attachments", "update"],
489
+ scan: ["attachments", "update"]
490
+ };
491
+ var DATABASE_REQUIREMENTS = {
492
+ info: NONE,
493
+ getFulltextSetup: READ,
494
+ getExtractionSetup: READ,
495
+ getSummarySetup: READ,
496
+ setFulltextSetup: ["update"],
497
+ setExtractionSetup: ["update"],
498
+ setSummarySetup: ["update"]
499
+ };
500
+ var MockForbiddenError = class extends Error {
501
+ name = "forbidden";
502
+ };
503
+ function guardApi(api, requirementOf, check) {
504
+ const guarded = {};
505
+ for (const [key, value] of Object.entries(api)) {
506
+ guarded[key] = typeof value === "function" ? (...args) => {
507
+ try {
508
+ check(key, requirementOf(key));
509
+ } catch (error) {
510
+ return Promise.reject(error);
511
+ }
512
+ return value.apply(api, args);
513
+ } : value;
514
+ }
515
+ return guarded;
516
+ }
517
+ function guardDatabase(database, currentInfo, enforce) {
518
+ const check = (area) => (method, requirement) => {
519
+ if (!enforce()) return;
520
+ const info = currentInfo();
521
+ for (const capability of requirement) {
522
+ if (!info.capabilities.includes(capability)) {
523
+ throw new MockForbiddenError(
524
+ `Database "${info.id}" was not granted the "${capability}" capability (${area}${method})`
525
+ );
526
+ }
527
+ }
528
+ };
529
+ const predict = (fn, capability) => async (...args) => {
530
+ const info = currentInfo();
531
+ if (enforce() && !info.capabilities.includes(capability)) {
532
+ return {
533
+ allowed: false,
534
+ reason: `database "${info.id}" was not granted the "${capability}" capability`,
535
+ tier: "tier1"
536
+ };
537
+ }
538
+ return await fn.apply(database.documents, args);
539
+ };
540
+ const base = guardApi(
541
+ database,
542
+ (method) => DATABASE_REQUIREMENTS[method] ?? READ,
543
+ check("")
544
+ );
545
+ return {
546
+ ...base,
547
+ async info() {
548
+ const info = currentInfo();
549
+ return { ...info, capabilities: [...info.capabilities] };
550
+ },
551
+ documents: {
552
+ ...guardApi(
553
+ database.documents,
554
+ (method) => DOCUMENT_REQUIREMENTS[method] ?? READ,
555
+ check("documents.")
556
+ ),
557
+ // the non-throwing predictions report the missing capability instead
558
+ canCreate: predict(database.documents.canCreate, "create"),
559
+ canChange: predict(database.documents.canChange, "update"),
560
+ canDelete: predict(database.documents.canDelete, "delete"),
561
+ canUndelete: predict(database.documents.canUndelete, "delete")
562
+ },
563
+ attachments: guardApi(
564
+ database.attachments,
565
+ (method) => ATTACHMENT_REQUIREMENTS[method] ?? ["attachments"],
566
+ check("attachments.")
567
+ ),
568
+ identity: guardApi(database.identity, () => ["sign"], check("identity.")),
569
+ timestamps: guardApi(database.timestamps, () => ["timestamps"], check("timestamps.")),
570
+ directory: guardApi(database.directory, () => ["directory"], check("directory."))
571
+ };
572
+ }
573
+
574
+ // src/testing/memoryAttachments.ts
575
+ function concat(chunks) {
576
+ const bytes = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
577
+ let offset = 0;
578
+ for (const chunk of chunks) {
579
+ bytes.set(chunk, offset);
580
+ offset += chunk.length;
581
+ }
582
+ return bytes;
583
+ }
584
+ function createMemoryAttachments() {
585
+ const files = /* @__PURE__ */ new Map();
586
+ const forDoc = (docId) => {
587
+ let map = files.get(docId);
588
+ if (!map) files.set(docId, map = /* @__PURE__ */ new Map());
589
+ return map;
590
+ };
591
+ return {
592
+ async list(docId) {
593
+ return [...forDoc(docId)].map(
594
+ ([fileName, file]) => ({
595
+ attachmentId: `${docId}/${fileName}`,
596
+ fileName,
597
+ mimeType: file.mimeType,
598
+ size: file.bytes.length
599
+ })
600
+ );
601
+ },
602
+ async remove(docId, attachmentName) {
603
+ forDoc(docId).delete(attachmentName);
604
+ return { ok: true };
605
+ },
606
+ async openReadStream(docId, attachmentName) {
607
+ const file = forDoc(docId).get(attachmentName);
608
+ if (!file) throw new Error(`Attachment not found: ${attachmentName}`);
609
+ let done = false;
610
+ return {
611
+ async read() {
612
+ if (done) return null;
613
+ done = true;
614
+ return file.bytes.slice();
615
+ },
616
+ async close() {
617
+ }
618
+ };
619
+ },
620
+ async openWriteStream(docId, attachmentName, contentType) {
621
+ const chunks = [];
622
+ return {
623
+ async write(chunk) {
624
+ chunks.push(new Uint8Array(chunk));
625
+ },
626
+ async close() {
627
+ forDoc(docId).set(attachmentName, {
628
+ bytes: concat(chunks),
629
+ mimeType: contentType ?? "application/octet-stream"
630
+ });
631
+ },
632
+ async abort() {
633
+ chunks.length = 0;
634
+ }
635
+ };
636
+ }
637
+ };
638
+ }
639
+
457
640
  // src/testing/browserTestHost.ts
641
+ var ALL_CAPABILITIES = [
642
+ "read",
643
+ "create",
644
+ "update",
645
+ "delete",
646
+ "history",
647
+ "attachments",
648
+ "views",
649
+ "directory",
650
+ "sign",
651
+ "timestamps",
652
+ "sealedchannel"
653
+ ];
458
654
  function capabilitiesForDefinitionPermissions(permissions) {
459
655
  const result = /* @__PURE__ */ new Set(["read"]);
460
656
  for (const permission of permissions ?? []) {
@@ -467,32 +663,104 @@ function capabilitiesForDefinitionPermissions(permissions) {
467
663
  }
468
664
  return [...result];
469
665
  }
470
- function mockDatabasesFromDefinition(definition, seed = {}) {
471
- return (definition.databases ?? []).map((database) => ({
472
- info: {
473
- id: database.logicalDatabaseId,
474
- title: database.label ?? database.logicalDatabaseId,
475
- capabilities: capabilitiesForDefinitionPermissions(database.permissions)
476
- },
477
- documents: seed[database.logicalDatabaseId]
478
- }));
666
+ function mockDatabasesFromDefinition(definition, seed = {}, options = {}) {
667
+ return (definition.databases ?? []).filter((database) => options.overrides?.[database.logicalDatabaseId]?.mapped !== false).map((database) => {
668
+ const override = options.overrides?.[database.logicalDatabaseId];
669
+ return {
670
+ info: {
671
+ id: database.logicalDatabaseId,
672
+ title: override?.title ?? database.label ?? database.logicalDatabaseId,
673
+ capabilities: override?.capabilities ? [...new Set(override.capabilities)] : capabilitiesForDefinitionPermissions(database.permissions)
674
+ },
675
+ documents: seed[database.logicalDatabaseId],
676
+ ...options.attachments === "none" ? {} : { methods: { attachments: createMemoryAttachments() } }
677
+ };
678
+ });
679
+ }
680
+ function readTestHostUrlSettings(search) {
681
+ const params = new URLSearchParams(search);
682
+ const settings = { overrides: {} };
683
+ for (const entry of params.getAll("db")) {
684
+ const separator = entry.indexOf(":");
685
+ if (separator <= 0) continue;
686
+ const id = entry.slice(0, separator);
687
+ const list = entry.slice(separator + 1);
688
+ settings.overrides[id] = list === "none" ? { mapped: false } : {
689
+ capabilities: list.split(",").map((capability) => capability.trim()).filter(
690
+ (capability) => ALL_CAPABILITIES.includes(capability)
691
+ )
692
+ };
693
+ }
694
+ const enforce = params.get("enforce");
695
+ if (enforce !== null) settings.enforceCapabilities = enforce === "1" || enforce === "true";
696
+ const users = Number.parseInt(params.get("users") ?? "", 10);
697
+ if (Number.isFinite(users) && users >= 0) settings.directoryUserCount = users;
698
+ const locale = params.get("locale");
699
+ if (locale) settings.locale = locale;
700
+ const theme = params.get("theme");
701
+ if (theme === "dark" || theme === "light") settings.theme = theme;
702
+ return settings;
479
703
  }
480
704
  function withLaunchId(appUrl, launchId) {
481
705
  const url = new URL(appUrl, window.location.href);
482
706
  url.searchParams.set("mindoodbAppLaunchId", launchId);
483
707
  return url.toString();
484
708
  }
709
+ var TINY_PNG = Uint8Array.from(
710
+ atob(
711
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN4+O7dfwAJYgPUBHcDcgAAAABJRU5ErkJggg=="
712
+ ),
713
+ (char) => char.charCodeAt(0)
714
+ );
715
+ async function sampleScan() {
716
+ const canvas = document.createElement("canvas");
717
+ canvas.width = 1200;
718
+ canvas.height = 900;
719
+ const g = typeof canvas.toBlob === "function" ? canvas.getContext("2d") : null;
720
+ if (!g) {
721
+ return { fileName: "sample-scan.png", mimeType: "image/png", bytes: TINY_PNG };
722
+ }
723
+ g.fillStyle = "#f4f1ea";
724
+ g.fillRect(0, 0, canvas.width, canvas.height);
725
+ g.fillStyle = "#1f2a44";
726
+ g.font = "bold 64px sans-serif";
727
+ g.fillText("Scanned page", 90, 150);
728
+ g.fillStyle = "#8a93a6";
729
+ for (let line = 0; line < 9; line += 1) {
730
+ g.fillRect(90, 240 + line * 60, line % 3 === 2 ? 620 : 1020, 18);
731
+ }
732
+ g.strokeStyle = "#c0392b";
733
+ g.lineWidth = 8;
734
+ g.beginPath();
735
+ g.arc(960, 740, 110, 0, Math.PI * 2);
736
+ g.stroke();
737
+ g.fillStyle = "#c0392b";
738
+ g.font = "bold 40px sans-serif";
739
+ g.fillText("TEST", 910, 755);
740
+ const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", 0.9));
741
+ if (!blob) {
742
+ return { fileName: "sample-scan.png", mimeType: "image/png", bytes: TINY_PNG };
743
+ }
744
+ return {
745
+ fileName: "sample-scan.jpg",
746
+ mimeType: "image/jpeg",
747
+ bytes: new Uint8Array(await blob.arrayBuffer())
748
+ };
749
+ }
485
750
  function createBrowserTestHost(options) {
486
- const { frame, appUrl, onChange, ...hostOptions } = options;
487
- let nextScan = null;
751
+ const { frame, appUrl, onChange, scanMode: initialScanMode, ...hostOptions } = options;
752
+ let nextScan;
753
+ let scanMode = initialScanMode ?? "cancel";
488
754
  let scanCounter = 0;
489
755
  const log = {
490
756
  notifications: [],
491
757
  previews: [],
492
758
  scans: [],
493
759
  focusRequests: 0,
494
- requests: []
760
+ requests: [],
761
+ responses: {}
495
762
  };
763
+ const changed = () => queueMicrotask(() => onChange?.());
496
764
  const host = createFakeBridgeHost({
497
765
  ...hostOptions,
498
766
  onRequest(request) {
@@ -515,29 +783,56 @@ function createBrowserTestHost(options) {
515
783
  });
516
784
  }
517
785
  hostOptions.onRequest?.(request);
518
- queueMicrotask(() => onChange?.());
786
+ changed();
787
+ },
788
+ onResponse(request, response) {
789
+ log.responses[request.id] = response;
790
+ hostOptions.onResponse?.(request, response);
791
+ changed();
519
792
  },
520
793
  requestHandlers: {
521
- "attachments.scanAndWrite": (params) => {
794
+ "attachments.scanAndWrite": async (params, { host: controller }) => {
522
795
  const input = params ?? {};
523
- const scan = nextScan;
524
- nextScan = null;
525
- log.scans.push({
526
- databaseId: String(input.databaseId),
527
- docId: String(input.docId),
528
- result: scan ? "scanned" : "cancelled"
529
- });
796
+ const databaseId = String(input.databaseId);
797
+ const docId = String(input.docId);
798
+ if (controller.getEnforceCapabilities()) {
799
+ const info = controller.listDatabases().find((entry) => entry.id === databaseId);
800
+ const missing = ["attachments", "update"].find(
801
+ (capability) => !info?.capabilities.includes(capability)
802
+ );
803
+ if (missing) {
804
+ const error = new Error(
805
+ `Database "${databaseId}" was not granted the "${missing}" capability (attachments.scan)`
806
+ );
807
+ error.name = "forbidden";
808
+ throw error;
809
+ }
810
+ }
811
+ const pending = nextScan;
812
+ nextScan = void 0;
813
+ const mode = pending === void 0 ? scanMode : pending;
814
+ const scan = mode === null || mode === "cancel" ? null : mode === "sample" ? await sampleScan() : mode;
530
815
  if (!scan) {
816
+ log.scans.push({ databaseId, docId, result: "cancelled" });
531
817
  return { ok: false };
532
818
  }
533
819
  scanCounter += 1;
820
+ const fileName = mode === "sample" && typeof input.defaultFileName === "string" ? input.defaultFileName : scan.fileName;
821
+ if (scan.bytes) {
822
+ const database = await controller.session.openDatabase(databaseId);
823
+ const writer = await database.attachments.openWriteStream(docId, fileName, scan.mimeType);
824
+ await writer.write(scan.bytes);
825
+ await writer.close();
826
+ }
827
+ const size = scan.bytes?.length ?? scan.size ?? 0;
828
+ log.scans.push({ databaseId, docId, result: "scanned", fileName, mimeType: scan.mimeType, size });
534
829
  return {
535
830
  ok: true,
536
831
  attachment: {
537
832
  attachmentId: `test-scan-${scanCounter}`,
538
- fileName: scan.fileName,
833
+ fileName,
539
834
  mimeType: scan.mimeType,
540
- size: scan.size
835
+ size
541
836
  }
542
837
  };
543
838
  },
@@ -552,12 +847,19 @@ function createBrowserTestHost(options) {
552
847
  };
553
848
  window.addEventListener("message", onMessage);
554
849
  frame.src = withLaunchId(appUrl, host.launchId);
850
+ const reloadApp = () => {
851
+ frame.src = withLaunchId(appUrl, host.launchId);
852
+ };
555
853
  return {
556
854
  host,
557
855
  log,
558
856
  setNextScan(scan) {
559
857
  nextScan = scan;
560
858
  },
859
+ setScanMode(mode) {
860
+ scanMode = mode;
861
+ },
862
+ getScanMode: () => scanMode,
561
863
  setTheme(theme) {
562
864
  host.emitThemeChange(theme);
563
865
  onChange?.();
@@ -566,37 +868,132 @@ function createBrowserTestHost(options) {
566
868
  host.emitHostFocusChange(focused);
567
869
  onChange?.();
568
870
  },
569
- reloadApp() {
570
- frame.src = withLaunchId(appUrl, host.launchId);
871
+ setLocale(locale) {
872
+ host.emitLocaleChange(locale);
873
+ onChange?.();
571
874
  },
875
+ setCapabilities(databaseId, capabilities) {
876
+ host.setCapabilities(databaseId, capabilities);
877
+ reloadApp();
878
+ onChange?.();
879
+ },
880
+ setEnforceCapabilities(enforce) {
881
+ host.setEnforceCapabilities(enforce);
882
+ onChange?.();
883
+ },
884
+ setDirectoryUsers(users) {
885
+ host.setDirectoryUsers(users);
886
+ onChange?.();
887
+ },
888
+ reloadApp,
572
889
  dispose() {
573
890
  window.removeEventListener("message", onMessage);
574
891
  host.dispose();
575
892
  }
576
893
  };
577
894
  }
895
+ var TEST_HOST_LOCALES = [
896
+ { tag: "en-US", label: "English" },
897
+ { tag: "de-DE", label: "Deutsch" },
898
+ { tag: "fr-FR", label: "Fran\xE7ais" },
899
+ { tag: "es-ES", label: "Espa\xF1ol" },
900
+ { tag: "it-IT", label: "Italiano" },
901
+ { tag: "pt-BR", label: "Portugu\xEAs" },
902
+ { tag: "nl-NL", label: "Nederlands" },
903
+ { tag: "pl-PL", label: "Polski" },
904
+ { tag: "cs-CZ", label: "\u010Ce\u0161tina" },
905
+ { tag: "ru-RU", label: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439" },
906
+ { tag: "zh-CN", label: "\u7B80\u4F53\u4E2D\u6587" },
907
+ { tag: "zh-TW", label: "\u7E41\u9AD4\u4E2D\u6587" },
908
+ { tag: "ja-JP", label: "\u65E5\u672C\u8A9E" },
909
+ { tag: "ko-KR", label: "\uD55C\uAD6D\uC5B4" },
910
+ { tag: "th-TH", label: "\u0E44\u0E17\u0E22" },
911
+ { tag: "vi-VN", label: "Ti\u1EBFng Vi\u1EC7t" },
912
+ { tag: "id-ID", label: "Bahasa Indonesia" },
913
+ { tag: "ms-MY", label: "Bahasa Melayu" },
914
+ { tag: "hi-IN", label: "\u0939\u093F\u0928\u094D\u0926\u0940" },
915
+ { tag: "ar-SA", label: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629" },
916
+ { tag: "he-IL", label: "\u05E2\u05D1\u05E8\u05D9\u05EA" }
917
+ ];
918
+ var DIRECTORY_SIZES = [0, 5, 25, 120, 500];
578
919
  var STYLE = `
579
- .htest{display:grid;grid-template-columns:minmax(0,1fr) 300px;height:100vh;margin:0;font:13px/1.45 system-ui,sans-serif;background:#eef1f7;color:#172033}
920
+ .htest{display:grid;grid-template-columns:minmax(0,1fr) 320px;height:100vh;margin:0;font:13px/1.45 system-ui,sans-serif;background:#eef1f7;color:#172033}
580
921
  .htest *{box-sizing:border-box}
581
922
  .htest__frame{width:100%;height:100%;border:0;background:#fff}
582
923
  .htest__panel{border-left:1px solid #d7dce8;overflow:auto;padding:14px;background:#f8f9fc}
583
924
  .htest__badge{display:inline-block;padding:2px 8px;border-radius:999px;background:#fde68a;color:#713f12;font-weight:600;font-size:11px;letter-spacing:.04em}
584
925
  .htest h1{font-size:15px;margin:8px 0 12px}
585
926
  .htest h2{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#5b6478;margin:16px 0 6px}
586
- .htest__row{display:flex;gap:6px;flex-wrap:wrap}
587
- .htest button{font:inherit;padding:5px 9px;border-radius:7px;border:1px solid #c9d0e0;background:#fff;cursor:pointer}
927
+ .htest h3{font-size:12px;margin:10px 0 4px}
928
+ .htest__row{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
929
+ .htest button,.htest select{font:inherit;padding:5px 9px;border-radius:7px;border:1px solid #c9d0e0;background:#fff;cursor:pointer;max-width:100%}
588
930
  .htest ul{list-style:none;margin:0;padding:0}
589
931
  .htest li{padding:5px 0;border-bottom:1px solid #e4e8f1;word-break:break-word}
590
932
  .htest__muted{color:#5b6478}
933
+ .htest__caps{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2px 8px}
934
+ .htest__caps label,.htest__check{display:flex;gap:5px;align-items:center}
935
+ .htest details summary{cursor:pointer;display:flex;gap:6px;align-items:baseline}
936
+ .htest details summary::-webkit-details-marker{display:none}
937
+ .htest details summary::before{content:"\u25B8";color:#8a93a6}
938
+ .htest details[open] summary::before{content:"\u25BE"}
939
+ .htest__meta{margin-left:auto;color:#5b6478;font-size:11px;white-space:nowrap}
940
+ .htest__error{color:#b42318}
941
+ .htest pre{margin:4px 0 2px;padding:6px 8px;background:#fff;border:1px solid #e4e8f1;border-radius:6px;font:11px/1.4 ui-monospace,monospace;white-space:pre-wrap;word-break:break-all;max-height:260px;overflow:auto}
591
942
  @media (max-width:720px){.htest{grid-template-columns:1fr;grid-template-rows:65vh auto}.htest__panel{border-left:0;border-top:1px solid #d7dce8}}
592
943
  `;
944
+ function previewJson(value, maxString = 400, maxItems = 30, maxDepth = 6) {
945
+ const seen = /* @__PURE__ */ new WeakSet();
946
+ const walk = (entry, depth) => {
947
+ if (entry instanceof Uint8Array || entry instanceof ArrayBuffer) {
948
+ const bytes = entry instanceof ArrayBuffer ? new Uint8Array(entry) : entry;
949
+ const hex = Array.from(bytes.subarray(0, 16), (byte) => byte.toString(16).padStart(2, "0")).join(" ");
950
+ return `<${bytes.length} bytes${bytes.length ? `: ${hex}${bytes.length > 16 ? " \u2026" : ""}` : ""}>`;
951
+ }
952
+ if (typeof entry === "string") {
953
+ return entry.length > maxString ? `${entry.slice(0, maxString)}\u2026 (${entry.length} chars)` : entry;
954
+ }
955
+ if (entry === null || typeof entry !== "object") return entry;
956
+ if (seen.has(entry)) return "<circular>";
957
+ if (depth >= maxDepth) return Array.isArray(entry) ? `<array(${entry.length})>` : "<object>";
958
+ seen.add(entry);
959
+ if (Array.isArray(entry)) {
960
+ const items = entry.slice(0, maxItems).map((child) => walk(child, depth + 1));
961
+ if (entry.length > maxItems) items.push(`\u2026 ${entry.length - maxItems} more`);
962
+ return items;
963
+ }
964
+ const result = {};
965
+ for (const [key, child] of Object.entries(entry)) result[key] = walk(child, depth + 1);
966
+ return result;
967
+ };
968
+ try {
969
+ return JSON.stringify(walk(value, 0), null, 2) ?? "undefined";
970
+ } catch {
971
+ return String(value);
972
+ }
973
+ }
593
974
  function item(text) {
594
975
  const li = document.createElement("li");
595
976
  li.textContent = text;
596
977
  return li;
597
978
  }
598
979
  function mountHavenTestHost(options) {
599
- const { container = document.body, title = "Haven test host", ...hostOptions } = options;
980
+ const {
981
+ container = document.body,
982
+ title = "Haven test host",
983
+ locales = TEST_HOST_LOCALES,
984
+ urlSettings: useUrl = true,
985
+ ...hostOptions
986
+ } = options;
987
+ const url = useUrl ? readTestHostUrlSettings(window.location.search) : { overrides: {} };
988
+ const databases = (hostOptions.databases ?? []).filter((database) => url.overrides[database.info.id]?.mapped !== false).map((database) => {
989
+ const capabilities = url.overrides[database.info.id]?.capabilities;
990
+ return capabilities ? { ...database, info: { ...database.info, capabilities } } : database;
991
+ });
992
+ const launchContext = {
993
+ ...hostOptions.launchContext,
994
+ ...url.locale ? { locale: url.locale } : {},
995
+ ...url.theme ? { theme: { mode: url.theme, preset: "mindoo" } } : {}
996
+ };
600
997
  const style = document.createElement("style");
601
998
  style.textContent = STYLE;
602
999
  document.head.append(style);
@@ -614,7 +1011,9 @@ function mountHavenTestHost(options) {
614
1011
  document.body.style.margin = "0";
615
1012
  }
616
1013
  let focused = false;
617
- let dark = false;
1014
+ let dark = url.theme === "dark";
1015
+ let initialDark = url.theme ? void 0 : false;
1016
+ const openRequests = /* @__PURE__ */ new Set();
618
1017
  const lists = {
619
1018
  notifications: document.createElement("ul"),
620
1019
  previews: document.createElement("ul"),
@@ -630,11 +1029,106 @@ function mountHavenTestHost(options) {
630
1029
  node.addEventListener("click", onClick);
631
1030
  return node;
632
1031
  }
1032
+ function hint(text) {
1033
+ const node = document.createElement("p");
1034
+ node.className = "htest__muted";
1035
+ node.textContent = text;
1036
+ return node;
1037
+ }
633
1038
  function section(label, ...children) {
634
1039
  const heading2 = document.createElement("h2");
635
1040
  heading2.textContent = label;
636
1041
  panel.append(heading2, ...children);
637
1042
  }
1043
+ function syncUrl() {
1044
+ if (!useUrl) return;
1045
+ const params = new URLSearchParams(window.location.search);
1046
+ for (const key of ["db", "enforce", "users", "locale", "theme"]) params.delete(key);
1047
+ const initial = new Map((hostOptions.databases ?? []).map((database) => [database.info.id, database]));
1048
+ for (const info of testHost.host.listDatabases()) {
1049
+ const requested = initial.get(info.id)?.info.capabilities ?? [];
1050
+ const same = requested.length === info.capabilities.length && requested.every((capability) => info.capabilities.includes(capability));
1051
+ if (!same) params.append("db", `${info.id}:${info.capabilities.join(",")}`);
1052
+ }
1053
+ for (const [id, override] of Object.entries(url.overrides)) {
1054
+ if (override.mapped === false) params.append("db", `${id}:none`);
1055
+ }
1056
+ if (testHost.host.getEnforceCapabilities()) params.set("enforce", "1");
1057
+ const users = testHost.host.getDirectoryUsers().length;
1058
+ if (users) params.set("users", String(users));
1059
+ if (currentLocale) params.set("locale", currentLocale);
1060
+ if (initialDark === void 0 || dark !== initialDark) params.set("theme", dark ? "dark" : "light");
1061
+ const query = params.toString();
1062
+ window.history.replaceState(
1063
+ null,
1064
+ "",
1065
+ `${window.location.pathname}${query ? `?${query}` : ""}${window.location.hash}`
1066
+ );
1067
+ }
1068
+ function requestEntry(request) {
1069
+ const li = document.createElement("li");
1070
+ const details = document.createElement("details");
1071
+ const response = testHost.log.responses[request.id];
1072
+ const params = request.params ?? {};
1073
+ const summary = document.createElement("summary");
1074
+ const method = document.createElement("span");
1075
+ method.textContent = request.method;
1076
+ if (response && !response.ok) method.className = "htest__error";
1077
+ const meta = document.createElement("span");
1078
+ meta.className = "htest__meta";
1079
+ meta.textContent = [
1080
+ typeof params.databaseId === "string" ? params.databaseId : null,
1081
+ response ? `${response.durationMs.toFixed(1)} ms` : "\u2026"
1082
+ ].filter(Boolean).join(" \xB7 ");
1083
+ summary.append(method, meta);
1084
+ details.append(summary);
1085
+ const fill = () => {
1086
+ if (details.childElementCount > 1) return;
1087
+ const body = document.createElement("div");
1088
+ const label = (text) => {
1089
+ const node = document.createElement("div");
1090
+ node.className = "htest__muted";
1091
+ node.textContent = text;
1092
+ return node;
1093
+ };
1094
+ const pre = (text, error = false) => {
1095
+ const node = document.createElement("pre");
1096
+ node.textContent = text;
1097
+ if (error) node.className = "htest__error";
1098
+ return node;
1099
+ };
1100
+ body.append(label("params"), pre(previewJson(request.params ?? null)));
1101
+ if (!response) body.append(label("pending"));
1102
+ else if (response.ok) body.append(label("result"), pre(previewJson(response.result)));
1103
+ else body.append(label("error"), pre(`${response.error.code}: ${response.error.message}`, true));
1104
+ details.append(body);
1105
+ };
1106
+ details.addEventListener("toggle", () => {
1107
+ if (details.open) {
1108
+ openRequests.add(request.id);
1109
+ fill();
1110
+ } else {
1111
+ openRequests.delete(request.id);
1112
+ }
1113
+ });
1114
+ if (openRequests.has(request.id)) {
1115
+ details.open = true;
1116
+ fill();
1117
+ }
1118
+ li.append(details);
1119
+ return li;
1120
+ }
1121
+ function scanEntry(entry) {
1122
+ const li = document.createElement("li");
1123
+ const details = document.createElement("details");
1124
+ const summary = document.createElement("summary");
1125
+ summary.textContent = `${entry.result} \u2192 ${entry.docId}`;
1126
+ const pre = document.createElement("pre");
1127
+ pre.textContent = previewJson(entry);
1128
+ details.append(summary, pre);
1129
+ li.append(details);
1130
+ return li;
1131
+ }
638
1132
  function render() {
639
1133
  const { log } = testHost;
640
1134
  focusState.textContent = `focused: ${focused ? "yes" : "no"} \xB7 requests: ${log.focusRequests}`;
@@ -644,10 +1138,19 @@ function mountHavenTestHost(options) {
644
1138
  lists.previews.replaceChildren(
645
1139
  ...log.previews.map((entry) => item(`${entry.attachmentName} (${entry.databaseId}/${entry.docId})`))
646
1140
  );
647
- lists.scans.replaceChildren(...log.scans.map((entry) => item(`${entry.result} \u2192 ${entry.docId}`)));
648
- lists.requests.replaceChildren(...log.requests.slice(-30).reverse().map((entry) => item(entry.method)));
1141
+ lists.scans.replaceChildren(...log.scans.map(scanEntry));
1142
+ lists.requests.replaceChildren(...log.requests.slice(-50).reverse().map(requestEntry));
649
1143
  }
650
- const testHost = createBrowserTestHost({ ...hostOptions, frame, onChange: render });
1144
+ const testHost = createBrowserTestHost({
1145
+ ...hostOptions,
1146
+ databases,
1147
+ launchContext,
1148
+ enforceCapabilities: url.enforceCapabilities ?? hostOptions.enforceCapabilities,
1149
+ directoryUsers: url.directoryUserCount !== void 0 ? generateDirectoryUsers(url.directoryUserCount) : hostOptions.directoryUsers,
1150
+ frame,
1151
+ onChange: render
1152
+ });
1153
+ let currentLocale = url.locale;
651
1154
  const badge = document.createElement("span");
652
1155
  badge.className = "htest__badge";
653
1156
  badge.textContent = "TEST HOST \xB7 MOCK DATA";
@@ -660,6 +1163,7 @@ function mountHavenTestHost(options) {
660
1163
  button("Toggle theme", () => {
661
1164
  dark = !dark;
662
1165
  testHost.setTheme({ mode: dark ? "dark" : "light", preset: "mindoo" });
1166
+ syncUrl();
663
1167
  }),
664
1168
  button("Toggle focus", () => {
665
1169
  focused = !focused;
@@ -668,31 +1172,121 @@ function mountHavenTestHost(options) {
668
1172
  button("Reload app", () => testHost.reloadApp())
669
1173
  );
670
1174
  section("Host", hostRow, focusState);
1175
+ const language = document.createElement("select");
1176
+ language.dataset.testid = "haven-test-language";
1177
+ language.setAttribute("aria-label", "Language");
1178
+ for (const { tag, label } of locales) language.add(new Option(`${label} (${tag})`, tag));
1179
+ void testHost.host.session.getLaunchContext().then((context) => {
1180
+ if (![...language.options].some((option) => option.value === context.locale)) {
1181
+ language.add(new Option(context.locale, context.locale), 0);
1182
+ }
1183
+ language.value = context.locale;
1184
+ dark = context.theme.mode === "dark";
1185
+ if (!url.theme) initialDark = dark;
1186
+ });
1187
+ language.addEventListener("change", () => {
1188
+ currentLocale = language.value;
1189
+ testHost.setLocale(language.value);
1190
+ syncUrl();
1191
+ });
1192
+ section("Language", language, hint("Haven's locale; the app switches without a reload."));
1193
+ const enforce = document.createElement("label");
1194
+ enforce.className = "htest__check";
1195
+ const enforceBox = document.createElement("input");
1196
+ enforceBox.type = "checkbox";
1197
+ enforceBox.dataset.testid = "haven-test-enforce";
1198
+ enforceBox.checked = testHost.host.getEnforceCapabilities();
1199
+ enforceBox.addEventListener("change", () => {
1200
+ testHost.setEnforceCapabilities(enforceBox.checked);
1201
+ syncUrl();
1202
+ });
1203
+ enforce.append(enforceBox, "Enforce capabilities (reject calls not granted)");
1204
+ const databaseBlocks = testHost.host.listDatabases().map((info) => {
1205
+ const block = document.createElement("div");
1206
+ const name = document.createElement("h3");
1207
+ name.textContent = info.title === info.id ? info.id : `${info.title} (${info.id})`;
1208
+ const grid = document.createElement("div");
1209
+ grid.className = "htest__caps";
1210
+ const boxes = /* @__PURE__ */ new Map();
1211
+ for (const capability of ALL_CAPABILITIES) {
1212
+ const label = document.createElement("label");
1213
+ const box = document.createElement("input");
1214
+ box.type = "checkbox";
1215
+ box.checked = info.capabilities.includes(capability);
1216
+ box.dataset.testid = `haven-test-cap-${info.id}-${capability}`;
1217
+ box.addEventListener("change", () => {
1218
+ const granted = ALL_CAPABILITIES.filter((entry) => boxes.get(entry)?.checked);
1219
+ testHost.setCapabilities(info.id, granted);
1220
+ syncUrl();
1221
+ });
1222
+ boxes.set(capability, box);
1223
+ label.append(box, capability);
1224
+ grid.append(label);
1225
+ }
1226
+ block.append(name, grid);
1227
+ return block;
1228
+ });
1229
+ section(
1230
+ "Databases",
1231
+ enforce,
1232
+ ...databaseBlocks,
1233
+ hint("Changing a capability relaunches the app. Unmap a database with ?db=<id>:none in the URL.")
1234
+ );
1235
+ const directory = document.createElement("select");
1236
+ directory.dataset.testid = "haven-test-directory";
1237
+ directory.setAttribute("aria-label", "Directory users");
1238
+ const userCount = testHost.host.getDirectoryUsers().length;
1239
+ const sizes = DIRECTORY_SIZES.includes(userCount) ? DIRECTORY_SIZES : [...DIRECTORY_SIZES, userCount];
1240
+ for (const size of sizes.sort((a, b) => a - b)) {
1241
+ directory.add(new Option(size === userCount && !DIRECTORY_SIZES.includes(size) ? `${size} (custom)` : `${size} users`, String(size)));
1242
+ }
1243
+ directory.value = String(userCount);
1244
+ directory.addEventListener("change", () => {
1245
+ testHost.setDirectoryUsers(generateDirectoryUsers(Number(directory.value)));
1246
+ syncUrl();
1247
+ });
1248
+ section("Directory", directory, hint("Usernames directory.listUsers() returns, e.g. to test paging."));
1249
+ const scan = document.createElement("select");
1250
+ scan.dataset.testid = "haven-test-scan-mode";
1251
+ scan.setAttribute("aria-label", "Scanner result");
1252
+ scan.add(new Option("Cancel the scan", "cancel"));
1253
+ scan.add(new Option("Sample image", "sample"));
1254
+ scan.add(new Option("Chosen file", "file"));
671
1255
  const scanInput = document.createElement("input");
672
1256
  scanInput.type = "file";
673
1257
  scanInput.accept = "image/jpeg,image/png,application/pdf";
674
- scanInput.addEventListener("change", () => {
1258
+ const initialMode = testHost.getScanMode();
1259
+ scan.value = typeof initialMode === "string" ? initialMode : "file";
1260
+ scanInput.hidden = scan.value !== "file";
1261
+ const applyScanMode = async () => {
1262
+ scanInput.hidden = scan.value !== "file";
1263
+ if (scan.value !== "file") {
1264
+ testHost.setScanMode(scan.value);
1265
+ return;
1266
+ }
675
1267
  const file = scanInput.files?.[0];
676
- testHost.setNextScan(
1268
+ testHost.setScanMode(
677
1269
  file ? {
678
1270
  fileName: file.name,
679
1271
  mimeType: file.type === "image/png" || file.type === "application/pdf" ? file.type : "image/jpeg",
680
- size: file.size
681
- } : null
1272
+ bytes: new Uint8Array(await file.arrayBuffer())
1273
+ } : "cancel"
682
1274
  );
683
- });
684
- const scanHint = document.createElement("p");
685
- scanHint.className = "htest__muted";
686
- scanHint.textContent = "The app's next scan returns this file. Without one it is cancelled.";
687
- section("Next scan", scanInput, scanHint);
1275
+ };
1276
+ scan.addEventListener("change", () => void applyScanMode());
1277
+ scanInput.addEventListener("change", () => void applyScanMode());
1278
+ section(
1279
+ "Scanner",
1280
+ scan,
1281
+ scanInput,
1282
+ hint("What attachments.scan() returns; the file is attached to the document like in Haven.")
1283
+ );
688
1284
  section("Notifications", lists.notifications);
689
1285
  section("Previews", lists.previews);
690
1286
  section("Scans", lists.scans);
691
- section("Requests (latest first)", lists.requests);
692
- void testHost.host.session.getLaunchContext().then((context) => {
693
- dark = context.theme.mode === "dark";
694
- });
1287
+ section("Requests (latest first, click for details)", lists.requests);
695
1288
  render();
1289
+ syncUrl();
696
1290
  window.__havenTestHost = testHost;
697
1291
  return testHost;
698
1292
  }
@@ -1864,7 +2458,7 @@ async function mockSha256Hex(value) {
1864
2458
  const digest = await subtle.digest("SHA-256", new TextEncoder().encode(value));
1865
2459
  return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
1866
2460
  }
1867
- function createDatabaseHandle(definition, peers) {
2461
+ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
1868
2462
  let createCounter = 0;
1869
2463
  let changeCounter = 0;
1870
2464
  const defaultViewFactory = async () => createDefaultViewNavigator();
@@ -2430,9 +3024,7 @@ function createDatabaseHandle(definition, peers) {
2430
3024
  status: "unknown"
2431
3025
  }));
2432
3026
  },
2433
- async listUsers() {
2434
- return [];
2435
- }
3027
+ listUsers: (async (options) => options ? pageUsers(directoryUsers(), options) : [...directoryUsers()])
2436
3028
  };
2437
3029
  const defaultViews = {
2438
3030
  async create(_input) {
@@ -2650,15 +3242,23 @@ function createMockSessionState(options = {}) {
2650
3242
  capabilities: [...definition.info.capabilities]
2651
3243
  }));
2652
3244
  for (const definition of definitions) {
2653
- const created = createDatabaseHandle(definition, databasePeers);
2654
- databaseHandles.set(definition.info.id, created.handle);
3245
+ const created = createDatabaseHandle(definition, databasePeers, () => directoryUsers);
3246
+ const databaseId = definition.info.id;
3247
+ databaseHandles.set(
3248
+ databaseId,
3249
+ guardDatabase(
3250
+ created.handle,
3251
+ () => databaseInfos.find((entry) => entry.id === databaseId) ?? definition.info,
3252
+ () => enforceCapabilities
3253
+ )
3254
+ );
2655
3255
  databaseViewDocumentLists.set(definition.info.id, created.listViewDocuments);
2656
3256
  databaseDocumentStores.set(definition.info.id, created.documents);
2657
3257
  databaseLiveQueryRefreshers.set(definition.info.id, created.notifyLiveQueries);
2658
3258
  databaseViewApis.set(definition.info.id, {
2659
3259
  async create(input) {
2660
- const documents = input.databaseIds.flatMap((databaseId) => {
2661
- const list = databaseViewDocumentLists.get(databaseId);
3260
+ const documents = input.databaseIds.flatMap((databaseId2) => {
3261
+ const list = databaseViewDocumentLists.get(databaseId2);
2662
3262
  return list ? list() : [];
2663
3263
  });
2664
3264
  const evaluating = await createEvaluatingViewNavigator(input, documents);
@@ -2678,6 +3278,16 @@ function createMockSessionState(options = {}) {
2678
3278
  databases: databaseInfos
2679
3279
  });
2680
3280
  };
3281
+ let enforceCapabilities = options.enforceCapabilities ?? false;
3282
+ let directoryUsers = [...options.directoryUsers ?? []];
3283
+ const setCapabilities = (databaseId, capabilities) => {
3284
+ const entry = databaseInfos.find((info) => info.id === databaseId);
3285
+ if (!entry) {
3286
+ throw new Error(`Unknown test database: ${databaseId}`);
3287
+ }
3288
+ entry.capabilities = [...new Set(capabilities)];
3289
+ launchContext = mergeLaunchContext(launchContext, { databases: databaseInfos });
3290
+ };
2681
3291
  setDatabases(options.databases ?? []);
2682
3292
  const session = {
2683
3293
  async getLaunchContext() {
@@ -2885,6 +3495,15 @@ function createMockSessionState(options = {}) {
2885
3495
  }));
2886
3496
  },
2887
3497
  setDatabases,
3498
+ setCapabilities,
3499
+ setEnforceCapabilities(enforce) {
3500
+ enforceCapabilities = enforce;
3501
+ },
3502
+ getEnforceCapabilities: () => enforceCapabilities,
3503
+ setDirectoryUsers(users) {
3504
+ directoryUsers = [...users];
3505
+ },
3506
+ getDirectoryUsers: () => [...directoryUsers],
2888
3507
  getDatabase(databaseId) {
2889
3508
  const database = databaseHandles.get(databaseId);
2890
3509
  if (!database) {
@@ -2936,6 +3555,15 @@ function createMockSessionState(options = {}) {
2936
3555
  }
2937
3556
  };
2938
3557
  }
3558
+ function generateDirectoryUsers(count, options = {}) {
3559
+ const prefix = options.prefix ?? "Test User";
3560
+ const organization = options.organization ?? "Test";
3561
+ const width = String(Math.max(count, 1)).length;
3562
+ return Array.from(
3563
+ { length: count },
3564
+ (_, index) => `CN=${prefix} ${String(index + 1).padStart(width, "0")}/O=${organization}`
3565
+ );
3566
+ }
2939
3567
  function createMockMindooDBAppSession(options = {}) {
2940
3568
  const state = createMockSessionState(options);
2941
3569
  return {
@@ -2947,6 +3575,9 @@ function createMockMindooDBAppSession(options = {}) {
2947
3575
  proposedApps: state.proposedApps,
2948
3576
  listDatabases: state.listDatabaseInfos,
2949
3577
  setDatabases: state.setDatabases,
3578
+ setCapabilities: state.setCapabilities,
3579
+ setEnforceCapabilities: state.setEnforceCapabilities,
3580
+ setDirectoryUsers: state.setDirectoryUsers,
2950
3581
  emitThemeChange: state.emitThemeChange,
2951
3582
  emitViewportChange: state.emitViewportChange,
2952
3583
  emitUiPreferencesChange: state.emitUiPreferencesChange,
@@ -3392,8 +4023,16 @@ function createFakeBridgeHost(options = {}) {
3392
4023
  return await state.getDatabase(String(params.databaseId)).directory.excerpt(
3393
4024
  Array.isArray(params.publicKeys) ? params.publicKeys.map(String) : []
3394
4025
  );
3395
- case "directory.listUsers":
3396
- return await state.getDatabase(String(params.databaseId)).directory.listUsers();
4026
+ case "directory.listUsers": {
4027
+ const directory = state.getDatabase(String(params.databaseId)).directory;
4028
+ const paged = params.query !== void 0 || params.cursor !== void 0 || params.limit !== void 0;
4029
+ if (!paged) return await directory.listUsers();
4030
+ return await directory.listUsers({
4031
+ query: typeof params.query === "string" ? params.query : void 0,
4032
+ cursor: typeof params.cursor === "string" ? params.cursor : void 0,
4033
+ limit: typeof params.limit === "number" ? params.limit : void 0
4034
+ });
4035
+ }
3397
4036
  case "attachments.list":
3398
4037
  return await state.getDatabase(String(params.databaseId)).attachments.list(
3399
4038
  String(params.docId),
@@ -3674,6 +4313,23 @@ function createFakeBridgeHost(options = {}) {
3674
4313
  if (message.kind === "request") {
3675
4314
  requests.push(message);
3676
4315
  options.onRequest?.(message);
4316
+ const startedAt = performance.now();
4317
+ const succeed = (result) => {
4318
+ postRpcSuccess(port, message.id, result);
4319
+ options.onResponse?.(message, {
4320
+ ok: true,
4321
+ result,
4322
+ durationMs: performance.now() - startedAt
4323
+ });
4324
+ };
4325
+ const fail = (error) => {
4326
+ postRpcError(port, message.id, error);
4327
+ options.onResponse?.(message, {
4328
+ ok: false,
4329
+ error,
4330
+ durationMs: performance.now() - startedAt
4331
+ });
4332
+ };
3677
4333
  try {
3678
4334
  const customHandler = customRequestHandlers.get(message.method);
3679
4335
  if (customHandler) {
@@ -3682,20 +4338,20 @@ function createFakeBridgeHost(options = {}) {
3682
4338
  request: message,
3683
4339
  port
3684
4340
  });
3685
- postRpcSuccess(port, message.id, result);
4341
+ succeed(result);
3686
4342
  return;
3687
4343
  }
3688
4344
  const builtinResult = await handleBuiltinRequest(message);
3689
4345
  if (builtinResult === void 0 && message.method !== "viewNavigators.dispose") {
3690
- postRpcError(port, message.id, {
4346
+ fail({
3691
4347
  code: "method-not-found",
3692
4348
  message: `No fake bridge handler is configured for ${message.method}.`
3693
4349
  });
3694
4350
  return;
3695
4351
  }
3696
- postRpcSuccess(port, message.id, builtinResult);
4352
+ succeed(builtinResult);
3697
4353
  } catch (error) {
3698
- postRpcError(port, message.id, createBridgeErrorPayload(error));
4354
+ fail(createBridgeErrorPayload(error));
3699
4355
  }
3700
4356
  return;
3701
4357
  }
@@ -3737,6 +4393,12 @@ function createFakeBridgeHost(options = {}) {
3737
4393
  },
3738
4394
  install,
3739
4395
  acceptConnection,
4396
+ listDatabases: state.listDatabaseInfos,
4397
+ setCapabilities: state.setCapabilities,
4398
+ setEnforceCapabilities: state.setEnforceCapabilities,
4399
+ getEnforceCapabilities: state.getEnforceCapabilities,
4400
+ setDirectoryUsers: state.setDirectoryUsers,
4401
+ getDirectoryUsers: state.getDirectoryUsers,
3740
4402
  dispose() {
3741
4403
  restoreWindow();
3742
4404
  connectedPorts.forEach((port) => {
@@ -3853,13 +4515,20 @@ function createFakeBridgeHost(options = {}) {
3853
4515
  return controller;
3854
4516
  }
3855
4517
  export {
4518
+ ALL_CAPABILITIES,
4519
+ MockForbiddenError,
4520
+ TEST_HOST_LOCALES,
3856
4521
  capabilitiesForDefinitionPermissions,
3857
4522
  createBrowserTestHost,
3858
4523
  createEvaluatingViewNavigator,
3859
4524
  createFakeBridgeHost,
4525
+ createMemoryAttachments,
3860
4526
  createMockMindooDBAppBridge,
3861
4527
  createMockMindooDBAppSession,
4528
+ generateDirectoryUsers,
3862
4529
  loadMindoodbForTesting,
3863
4530
  mockDatabasesFromDefinition,
3864
- mountHavenTestHost
4531
+ mountHavenTestHost,
4532
+ previewJson,
4533
+ readTestHostUrlSettings
3865
4534
  };