wdio-obsidian-service 3.0.4 → 3.1.0

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.
package/dist/index.d.ts CHANGED
@@ -471,6 +471,16 @@ interface ObsidianCapabilityOptions {
471
471
  * Path to the app asar to load into obsidian. If omitted it will be downloaded automatically.
472
472
  */
473
473
  appPath?: string;
474
+ /**
475
+ * Where to store test vaults when test on Android.
476
+ *
477
+ * Options:
478
+ * - "app-storage": Store the vault as Obsidian app data. Default.
479
+ * - "device-storage": Store the vault on the SD card (`/storage/emulated/0`). Currently not recommended due to
480
+ * intermittent write failures in Capacitor that cause flaky tests. See https://forum.obsidian.md/t/102935
481
+ * - custom path: Store vaults under a custom device storage path. Same caveats as "device-storage" apply.
482
+ */
483
+ androidVaultStorage?: string;
474
484
  }
475
485
  /**
476
486
  * Options passed to wdio-obsidian-service service in wdio.conf.mts. E.g.
@@ -563,14 +573,27 @@ declare class ObsidianWorkerService implements Services.ServiceInstance {
563
573
  */
564
574
  private electronSetupConfigDir;
565
575
  /**
566
- * Sets up the Obsidian app. Installs and launches it if needed, and sets permissions and contexts.
576
+ * Transfers src to dest. src may be a file or a directory.
577
+ * Handles making sure the permissions under Obsidian app storage are correct.
578
+ */
579
+ private appiumUpload;
580
+ /**
581
+ * Sets up the Obsidian app. Installs and launches it if needed, and sets permissions and contexts. Opens the vault
582
+ * switcher page.
567
583
  *
568
584
  * You can configure Appium to install and launch the app for you. However, if you do that it reboots the app after
569
585
  * every session/spec which is really slow. So we are handling the app setup manually here.
570
586
  */
571
587
  private appiumSetupApp;
588
+ /**
589
+ * Uploads the vault to appium
590
+ */
591
+ private appiumUploadVault;
572
592
  /**
573
593
  * Opens the vault in appium.
594
+ *
595
+ * clearAppState determines whether we wipe localStorage or not. (In desktop mode we create a entirely new
596
+ * electron dir every time we open a new vault, in Android we need to use clearAppState to replicate that.)
574
597
  */
575
598
  private appiumOpenVault;
576
599
  /**
package/dist/index.js CHANGED
@@ -1987,7 +1987,7 @@ var ERROR_LOG_VALIDATOR = [
1987
1987
  ];
1988
1988
 
1989
1989
  // src/service.ts
1990
- import { fileURLToPath as fileURLToPath3 } from "url";
1990
+ import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
1991
1991
  import ObsidianLauncher from "obsidian-launcher";
1992
1992
 
1993
1993
  // src/browserCommands.ts
@@ -2196,12 +2196,38 @@ function normalizePath(p) {
2196
2196
  p = p.normalize("NFC");
2197
2197
  return p;
2198
2198
  }
2199
+ function pathIsUnder(parent2, child) {
2200
+ const rel = path2.relative(path2.resolve(parent2), path2.resolve(child));
2201
+ return rel != "" && rel.split(path2.sep)[0] != ".." && !path2.isAbsolute(rel);
2202
+ }
2199
2203
  function isHidden(file) {
2200
2204
  return file.split("/").some((p) => p.startsWith("."));
2201
2205
  }
2202
2206
  function isText(file) {
2203
2207
  return [".md", ".json", ".txt", ".js"].includes(path2.extname(file).toLocaleLowerCase());
2204
2208
  }
2209
+ async function sleep(ms) {
2210
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2211
+ }
2212
+ async function retry(func, opts = {}) {
2213
+ const { retries = 4, backoff = 1e3, retryIf = () => true } = opts;
2214
+ let attempt = 0;
2215
+ let error;
2216
+ while (attempt <= retries) {
2217
+ try {
2218
+ return await func(attempt);
2219
+ } catch (e) {
2220
+ error = e;
2221
+ }
2222
+ const delay = backoff * Math.random() + backoff * Math.pow(2, attempt);
2223
+ if (!retryIf(error) || attempt >= retries) {
2224
+ throw error;
2225
+ }
2226
+ await sleep(delay);
2227
+ attempt += 1;
2228
+ }
2229
+ throw error;
2230
+ }
2205
2231
 
2206
2232
  // src/pageobjects/obsidianPage.ts
2207
2233
  var ObsidianPage = class extends BasePage {
@@ -2426,34 +2452,25 @@ var ObsidianPage = class extends BasePage {
2426
2452
  await this.browser.executeObsidian(async ({ app, obsidian }, file2, isVaultFile, strContent2, binContent2) => {
2427
2453
  file2 = obsidian.normalizePath(file2);
2428
2454
  const parent2 = file2.split("/").slice(0, -1).join("/");
2429
- let success = false;
2430
- let retries = 0;
2431
- while (!success && retries < 8) {
2432
- if (isVaultFile) {
2433
- if (parent2 && !app.vault.getAbstractFileByPath(parent2)) {
2434
- await app.vault.createFolder(parent2);
2435
- }
2436
- const fileObj = app.vault.getAbstractFileByPath(file2);
2437
- strContent2 = strContent2 ?? atob(binContent2);
2438
- if (fileObj) {
2439
- await app.vault.modify(fileObj, strContent2);
2440
- } else {
2441
- await app.vault.create(file2, strContent2);
2442
- }
2455
+ if (isVaultFile) {
2456
+ if (parent2 && !app.vault.getAbstractFileByPath(parent2)) {
2457
+ await app.vault.createFolder(parent2);
2458
+ }
2459
+ const fileObj = app.vault.getAbstractFileByPath(file2);
2460
+ strContent2 = strContent2 ?? atob(binContent2);
2461
+ if (fileObj) {
2462
+ await app.vault.modify(fileObj, strContent2);
2443
2463
  } else {
2444
- await app.vault.adapter.mkdir(parent2);
2445
- if (strContent2) {
2446
- await app.vault.adapter.write(file2, strContent2);
2447
- } else {
2448
- const buffer = Uint8Array.from(atob(binContent2), (c) => c.charCodeAt(0)).buffer;
2449
- await app.vault.adapter.writeBinary(file2, buffer);
2450
- }
2464
+ await app.vault.create(file2, strContent2);
2465
+ }
2466
+ } else {
2467
+ await app.vault.adapter.mkdir(parent2);
2468
+ if (strContent2) {
2469
+ await app.vault.adapter.write(file2, strContent2);
2470
+ } else {
2471
+ const buffer = Uint8Array.from(atob(binContent2), (c) => c.charCodeAt(0)).buffer;
2472
+ await app.vault.adapter.writeBinary(file2, buffer);
2451
2473
  }
2452
- success = !obsidian.Platform.isAndroidApp || (strContent2?.length ?? binContent2.length) === 0 || (await app.vault.adapter.stat(file2)).size > 0;
2453
- retries++;
2454
- }
2455
- if (!success) {
2456
- throw new Error(`Failed to write file ${file2}`);
2457
2474
  }
2458
2475
  }, file, !isHidden(file) && isText(file), strContent, binContent);
2459
2476
  }
@@ -2746,7 +2763,9 @@ function getNormalizedObsidianOptions(cap) {
2746
2763
  return cap[OBSIDIAN_CAPABILITY_KEY];
2747
2764
  }
2748
2765
  var minSupportedObsidianVersion = "1.0.3";
2749
- var androidVaultsDir = "/storage/emulated/0/Documents/wdio-obsidian-service-vaults";
2766
+ var ANDROID_APP_STORAGE_DIR = "/storage/emulated/0/Android/data/md.obsidian/files";
2767
+ var ANDROID_DEFAULT_DEVICE_STORAGE_DIR = "/storage/emulated/0/Documents/wdio-obsidian-service-vaults";
2768
+ var OBSIDIAN_HANG_ERROR = /* @__PURE__ */ Symbol("hang");
2750
2769
  var ObsidianLauncherService = class {
2751
2770
  constructor(options, capabilities, config) {
2752
2771
  this.options = options;
@@ -2805,6 +2824,15 @@ var ObsidianLauncherService = class {
2805
2824
  if (!chromedriverDir) {
2806
2825
  chromedriverDir = path4.join(this.obsidianLauncher.cacheDir, "appium-chromedriver");
2807
2826
  }
2827
+ let androidVaultStorage = obsidianOptions.androidVaultStorage;
2828
+ if (!androidVaultStorage) {
2829
+ androidVaultStorage = semver2.lt(appVersion, "1.8.10") ? "device-storage" : "app-storage";
2830
+ }
2831
+ if (androidVaultStorage == "device-storage") {
2832
+ androidVaultStorage = ANDROID_DEFAULT_DEVICE_STORAGE_DIR;
2833
+ } else if (androidVaultStorage != "app-storage" && !androidVaultStorage.startsWith("/")) {
2834
+ throw Error(`androidVaultStorage must be "app-storage", "device-storage", or an absolute path on the Android device.`);
2835
+ }
2808
2836
  const normalizedObsidianOptions = {
2809
2837
  ...obsidianOptions,
2810
2838
  plugins,
@@ -2814,7 +2842,8 @@ var ObsidianLauncherService = class {
2814
2842
  appVersion,
2815
2843
  installerVersion: appVersion,
2816
2844
  emulateMobile: false,
2817
- testRunId: runId
2845
+ testRunId: runId,
2846
+ androidVaultStorage
2818
2847
  };
2819
2848
  cap[OBSIDIAN_CAPABILITY_KEY] = normalizedObsidianOptions;
2820
2849
  cap["appium:app"] = apk;
@@ -2958,7 +2987,36 @@ var ObsidianWorkerService = class {
2958
2987
  };
2959
2988
  }
2960
2989
  /**
2961
- * Sets up the Obsidian app. Installs and launches it if needed, and sets permissions and contexts.
2990
+ * Transfers src to dest. src may be a file or a directory.
2991
+ * Handles making sure the permissions under Obsidian app storage are correct.
2992
+ */
2993
+ async appiumUpload(src, dest) {
2994
+ const browser2 = this.browser;
2995
+ const isDirectory = (await fsAsync3.stat(src)).isDirectory();
2996
+ const isAppStorage = pathIsUnder(ANDROID_APP_STORAGE_DIR, dest);
2997
+ if (isAppStorage) {
2998
+ const tmp = `/storage/emulated/0/Documents/tmp-${crypto3.randomBytes(10).toString("base64url").replace(/[-_]/g, "0")}`;
2999
+ if (isDirectory) {
3000
+ await appiumUploadFiles(browser2, { src, dest: tmp });
3001
+ } else {
3002
+ await browser2.pushFile(tmp, (await fsAsync3.readFile(src)).toString("base64"));
3003
+ }
3004
+ await browser2.execute(async (from, to) => {
3005
+ const Filesystem = window.Capacitor.Plugins.Filesystem;
3006
+ await Filesystem.copy({ from, to });
3007
+ }, pathToFileURL(tmp).toString(), pathToFileURL(dest).toString());
3008
+ await browser2.execute("mobile: shell", { command: "rm", args: ["-rf", tmp] });
3009
+ } else {
3010
+ if (isDirectory) {
3011
+ await appiumUploadFiles(browser2, { src, dest });
3012
+ } else {
3013
+ await browser2.pushFile(dest, (await fsAsync3.readFile(src)).toString("base64"));
3014
+ }
3015
+ }
3016
+ }
3017
+ /**
3018
+ * Sets up the Obsidian app. Installs and launches it if needed, and sets permissions and contexts. Opens the vault
3019
+ * switcher page.
2962
3020
  *
2963
3021
  * You can configure Appium to install and launch the app for you. However, if you do that it reboots the app after
2964
3022
  * every session/spec which is really slow. So we are handling the app setup manually here.
@@ -3014,30 +3072,50 @@ var ObsidianWorkerService = class {
3014
3072
  window.location.replace("http://localhost/");
3015
3073
  }
3016
3074
  });
3017
- if (!obsidianOptions.vault) {
3018
- await this.appiumCloseVault();
3019
- }
3075
+ await this.appiumCloseVault();
3020
3076
  }
3021
3077
  /**
3022
- * Opens the vault in appium.
3078
+ * Uploads the vault to appium
3023
3079
  */
3024
- async appiumOpenVault() {
3080
+ async appiumUploadVault() {
3025
3081
  const browser2 = this.browser;
3026
3082
  const obsidianOptions = getNormalizedObsidianOptions(browser2.requestedCapabilities);
3083
+ const isAppStorage = obsidianOptions.androidVaultStorage == "app-storage";
3027
3084
  const runId = obsidianOptions.testRunId;
3028
- const pathHash = crypto3.createHash("SHA256").update(obsidianOptions.openVault).digest("base64url").replace(/[-_]/g, "0").slice(0, 10);
3085
+ const pathHash = crypto3.createHash("SHA256").update(obsidianOptions.openVault).digest("base64url").replace(/[-_]/g, "0").slice(0, 8);
3029
3086
  const basename = path4.basename(obsidianOptions.vault);
3030
- const androidVault = `${androidVaultsDir}/${basename}-${pathHash}-${runId}-${obsidianOptions.copy ? "" : "no"}copy`;
3031
- obsidianOptions.androidVault = androidVault;
3032
- if (!await appiumExists(browser2, androidVault)) {
3033
- await appiumUploadFiles(browser2, { src: obsidianOptions.openVault, dest: androidVault });
3034
- }
3035
- await browser2.execute(async (androidVault2) => {
3036
- localStorage.clear();
3037
- localStorage.setItem("mobile-external-vaults", JSON.stringify([androidVault2]));
3038
- localStorage.setItem("mobile-selected-vault", androidVault2);
3039
- localStorage.setItem(`enable-plugin-${androidVault2}`, "true");
3040
- }, androidVault);
3087
+ const androidVaultName = `${basename}-${pathHash}-${runId}-wdio-${obsidianOptions.copy ? "copy" : "nocopy"}`;
3088
+ const androidVaultPath = `${isAppStorage ? ANDROID_APP_STORAGE_DIR : obsidianOptions.androidVaultStorage}/${androidVaultName}`;
3089
+ obsidianOptions.androidVault = androidVaultPath;
3090
+ if (!await appiumExists(browser2, obsidianOptions.androidVault)) {
3091
+ await this.appiumUpload(obsidianOptions.openVault, obsidianOptions.androidVault);
3092
+ }
3093
+ }
3094
+ /**
3095
+ * Opens the vault in appium.
3096
+ *
3097
+ * clearAppState determines whether we wipe localStorage or not. (In desktop mode we create a entirely new
3098
+ * electron dir every time we open a new vault, in Android we need to use clearAppState to replicate that.)
3099
+ */
3100
+ async appiumOpenVault(opts) {
3101
+ const browser2 = this.browser;
3102
+ const obsidianOptions = getNormalizedObsidianOptions(browser2.requestedCapabilities);
3103
+ const isAppStorage = obsidianOptions.androidVaultStorage == "app-storage";
3104
+ await browser2.execute(async (androidVaultPath, isAppStorage2, clearAppState) => {
3105
+ if (clearAppState) {
3106
+ localStorage.clear();
3107
+ }
3108
+ if (isAppStorage2) {
3109
+ const androidVaultName = androidVaultPath.split("/").at(-1);
3110
+ localStorage.setItem("mobile-external-vaults", JSON.stringify([]));
3111
+ localStorage.setItem("mobile-selected-vault", androidVaultName);
3112
+ localStorage.setItem(`enable-plugin-${androidVaultName}`, "true");
3113
+ } else {
3114
+ localStorage.setItem("mobile-external-vaults", JSON.stringify([androidVaultPath]));
3115
+ localStorage.setItem("mobile-selected-vault", androidVaultPath);
3116
+ localStorage.setItem(`enable-plugin-${androidVaultPath}`, "true");
3117
+ }
3118
+ }, obsidianOptions.androidVault, isAppStorage, opts.clearAppState);
3041
3119
  await navigateAndWait(browser2, () => location.reload());
3042
3120
  }
3043
3121
  /**
@@ -3045,8 +3123,12 @@ var ObsidianWorkerService = class {
3045
3123
  */
3046
3124
  async appiumCloseVault() {
3047
3125
  const browser2 = this.browser;
3048
- if (await browser2.execute(() => localStorage.length > 0)) {
3049
- await browser2.execute(() => localStorage.clear());
3126
+ const isVaultOpen = await browser2.execute(() => !!localStorage.getItem("mobile-selected-vault"));
3127
+ if (isVaultOpen) {
3128
+ await browser2.execute(() => {
3129
+ localStorage.removeItem("mobile-external-vaults");
3130
+ localStorage.removeItem("mobile-selected-vault");
3131
+ });
3050
3132
  await navigateAndWait(browser2, () => location.reload());
3051
3133
  }
3052
3134
  }
@@ -3067,10 +3149,37 @@ var ObsidianWorkerService = class {
3067
3149
  }, width, height);
3068
3150
  }
3069
3151
  if (obsidianOptions.vault) {
3152
+ if (isAppium(browser2.requestedCapabilities) && semver2.gte(obsidianOptions.appVersion, "1.12.0")) {
3153
+ await retry(async (attempt) => {
3154
+ if (attempt > 0) {
3155
+ console.warn("Obsidian Android app stuck, relaunching...");
3156
+ await navigateAndWait(browser2, () => location.reload());
3157
+ }
3158
+ await browser2.waitUntil(
3159
+ () => browser2.execute(() => !!window.ready),
3160
+ // wait for enhance.js to be loaded
3161
+ { timeout: 6e4, interval: 100 }
3162
+ );
3163
+ await browser2.waitUntil(
3164
+ // if this fails to load soon after, we are in the hang state
3165
+ () => browser2.execute(() => document.body?.classList.contains("is-mobile")),
3166
+ { timeout: 2e3, interval: 100 }
3167
+ ).catch(() => {
3168
+ throw OBSIDIAN_HANG_ERROR;
3169
+ });
3170
+ }, {
3171
+ retries: 2,
3172
+ backoff: 2e3,
3173
+ retryIf: (error) => error === OBSIDIAN_HANG_ERROR
3174
+ });
3175
+ }
3070
3176
  await browser2.waitUntil(
3071
3177
  // wait until the helper plugin is loaded
3072
3178
  () => browser2.execute(() => !!window.wdioObsidianService),
3073
- { timeout: 6e4, interval: 100 }
3179
+ {
3180
+ timeout: isAppium(browser2.requestedCapabilities) ? 12e4 : 6e4,
3181
+ interval: 100
3182
+ }
3074
3183
  );
3075
3184
  await browser2.executeObsidian(async ({ app }) => {
3076
3185
  await new Promise((resolve2) => app.workspace.onLayoutReady(resolve2));
@@ -3104,9 +3213,10 @@ var ObsidianWorkerService = class {
3104
3213
  this.requestedCapabilities[OBSIDIAN_CAPABILITY_KEY] = newObsidianOptions;
3105
3214
  if (vault) {
3106
3215
  await service.setupVault(this.requestedCapabilities);
3107
- await service.appiumOpenVault();
3216
+ await service.appiumUploadVault();
3217
+ await service.appiumOpenVault({ clearAppState: true });
3108
3218
  } else {
3109
- await navigateAndWait(this, () => location.replace("http://localhost/_capacitor_file_/not-a-file"));
3219
+ await service.appiumCloseVault();
3110
3220
  const local = path4.join(oldObsidianOptions.openVault, ".obsidian");
3111
3221
  const localCommunityPlugins = path4.join(local, "community-plugins.json");
3112
3222
  const localAppearance = path4.join(local, "appearance.json");
@@ -3123,10 +3233,12 @@ var ObsidianWorkerService = class {
3123
3233
  plugins: selectedPlugins,
3124
3234
  themes: selectedThemes
3125
3235
  });
3126
- let files = [localCommunityPlugins, localAppearance];
3127
- files = (await Promise.all(files.map(async (f) => await fileExists(f) ? f : ""))).filter((f) => f);
3128
- await appiumUploadFiles(this, { src: local, dest: remote2, files });
3129
- await navigateAndWait(this, () => location.replace("http://localhost/"));
3236
+ for (const [src, dest] of [[localCommunityPlugins, remoteCommunityPlugins], [localAppearance, remoteAppearance]]) {
3237
+ if (await fileExists(src)) {
3238
+ await service.appiumUpload(src, dest);
3239
+ }
3240
+ }
3241
+ await service.appiumOpenVault({ clearAppState: false });
3130
3242
  }
3131
3243
  } else {
3132
3244
  const newCap = _3.cloneDeep(
@@ -3186,7 +3298,8 @@ var ObsidianWorkerService = class {
3186
3298
  if (isAppium(browser2.requestedCapabilities)) {
3187
3299
  await this.appiumSetupApp();
3188
3300
  if (cap[OBSIDIAN_CAPABILITY_KEY].vault) {
3189
- await this.appiumOpenVault();
3301
+ await this.appiumUploadVault();
3302
+ await this.appiumOpenVault({ clearAppState: true });
3190
3303
  }
3191
3304
  }
3192
3305
  await this.prepareApp();
@@ -3201,8 +3314,13 @@ var ObsidianWorkerService = class {
3201
3314
  const obsidianOptions = getNormalizedObsidianOptions(browser2.requestedCapabilities);
3202
3315
  if (isAppium(cap)) {
3203
3316
  await this.appiumCloseVault();
3317
+ const androidVaultsDir = obsidianOptions.androidVaultStorage == "app-storage" ? ANDROID_APP_STORAGE_DIR : obsidianOptions.androidVaultStorage;
3204
3318
  for (const file of await appiumReaddir(browser2, androidVaultsDir)) {
3205
- if (!file.includes(obsidianOptions.testRunId) || file.endsWith("-copy")) {
3319
+ const name = path4.posix.basename(file);
3320
+ const isServiceManaged = name.match(/-wdio-(no)?copy$/);
3321
+ const isFromThisRun = name.includes(obsidianOptions.testRunId);
3322
+ const isCopy = name.match(/-copy$/);
3323
+ if (isServiceManaged && (!isFromThisRun || isCopy)) {
3206
3324
  await browser2.execute("mobile: shell", { command: "rm", args: ["-rf", file] });
3207
3325
  }
3208
3326
  }