tauri-test-cli 0.6.2 → 0.7.1
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/cli.js +432 -47
- package/package.json +3 -2
- package/scripts/generate-skill-content.ts +28 -0
package/dist/cli.js
CHANGED
|
@@ -102312,7 +102312,7 @@ var init_esm11 = __esm(() => {
|
|
|
102312
102312
|
});
|
|
102313
102313
|
|
|
102314
102314
|
// src/cli.ts
|
|
102315
|
-
import { resolve } from "path";
|
|
102315
|
+
import { resolve as resolve2 } from "path";
|
|
102316
102316
|
|
|
102317
102317
|
// src/driver.ts
|
|
102318
102318
|
import { spawn, execSync } from "child_process";
|
|
@@ -120707,7 +120707,9 @@ async function connect(options) {
|
|
|
120707
120707
|
}
|
|
120708
120708
|
async function disconnect() {
|
|
120709
120709
|
if (browser) {
|
|
120710
|
-
|
|
120710
|
+
try {
|
|
120711
|
+
await browser.deleteSession();
|
|
120712
|
+
} catch {}
|
|
120711
120713
|
browser = null;
|
|
120712
120714
|
}
|
|
120713
120715
|
stopDriver();
|
|
@@ -120905,12 +120907,18 @@ async function waitForInteractive(selector, timeout = 5000) {
|
|
|
120905
120907
|
await element.waitForClickable({ timeout });
|
|
120906
120908
|
}
|
|
120907
120909
|
|
|
120910
|
+
// src/commands/utils.ts
|
|
120911
|
+
var xvfbDisplay = null;
|
|
120912
|
+
function getXvfbDisplay() {
|
|
120913
|
+
return xvfbDisplay;
|
|
120914
|
+
}
|
|
120915
|
+
|
|
120908
120916
|
// src/commands/screenshot.ts
|
|
120909
120917
|
var HTML2CANVAS_CDN = "https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js";
|
|
120910
120918
|
async function screenshot(options = {}) {
|
|
120911
120919
|
const browser3 = requireBrowser();
|
|
120912
120920
|
const autoWait = options.autoWait ?? true;
|
|
120913
|
-
const timeout = options.timeout ??
|
|
120921
|
+
const timeout = options.timeout ?? 15000;
|
|
120914
120922
|
if (autoWait) {
|
|
120915
120923
|
await waitForDomStable();
|
|
120916
120924
|
}
|
|
@@ -120921,12 +120929,21 @@ async function screenshot(options = {}) {
|
|
|
120921
120929
|
data2 = await withTimeout(captureWithHtml2Canvas(browser3), timeout, "html2canvas timed out");
|
|
120922
120930
|
method = "html2canvas";
|
|
120923
120931
|
} catch (err) {
|
|
120924
|
-
console.error(`html2canvas failed: ${err}, trying
|
|
120932
|
+
console.error(`html2canvas failed: ${err}, trying canvas fallback...`);
|
|
120925
120933
|
try {
|
|
120926
|
-
data2 = await withTimeout(browser3
|
|
120927
|
-
method = "
|
|
120928
|
-
} catch (
|
|
120929
|
-
|
|
120934
|
+
data2 = await withTimeout(captureWithCanvas(browser3), timeout, "Canvas screenshot timed out");
|
|
120935
|
+
method = "canvas";
|
|
120936
|
+
} catch (canvasErr) {
|
|
120937
|
+
if (getXvfbDisplay() !== null) {
|
|
120938
|
+
throw new Error(`All screenshot methods failed in Xvfb: html2canvas: ${err}, canvas: ${canvasErr}`);
|
|
120939
|
+
}
|
|
120940
|
+
console.error(`Canvas failed: ${canvasErr}, trying native...`);
|
|
120941
|
+
try {
|
|
120942
|
+
data2 = await withTimeout(browser3.takeScreenshot(), timeout, `Native screenshot timed out after ${timeout}ms`);
|
|
120943
|
+
method = "native";
|
|
120944
|
+
} catch (nativeErr) {
|
|
120945
|
+
throw new Error(`All screenshot methods failed: html2canvas: ${err}, canvas: ${canvasErr}, native: ${nativeErr}`);
|
|
120946
|
+
}
|
|
120930
120947
|
}
|
|
120931
120948
|
}
|
|
120932
120949
|
if (options.output) {
|
|
@@ -120993,6 +121010,61 @@ async function captureWithHtml2Canvas(browser3) {
|
|
|
120993
121010
|
}
|
|
120994
121011
|
return base64;
|
|
120995
121012
|
}
|
|
121013
|
+
async function captureWithCanvas(browser3) {
|
|
121014
|
+
const base64 = await browser3.executeAsync((done) => {
|
|
121015
|
+
try {
|
|
121016
|
+
const w2 = window.innerWidth || 800;
|
|
121017
|
+
const h = window.innerHeight || 600;
|
|
121018
|
+
const canvas = document.createElement("canvas");
|
|
121019
|
+
canvas.width = w2;
|
|
121020
|
+
canvas.height = h;
|
|
121021
|
+
const ctx = canvas.getContext("2d");
|
|
121022
|
+
ctx.fillStyle = "#ffffff";
|
|
121023
|
+
ctx.fillRect(0, 0, w2, h);
|
|
121024
|
+
const serializer = new XMLSerializer;
|
|
121025
|
+
const cloned = document.documentElement.cloneNode(true);
|
|
121026
|
+
const html3 = serializer.serializeToString(cloned);
|
|
121027
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w2}" height="${h}">
|
|
121028
|
+
<foreignObject width="100%" height="100%">
|
|
121029
|
+
${html3}
|
|
121030
|
+
</foreignObject>
|
|
121031
|
+
</svg>`;
|
|
121032
|
+
const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
|
|
121033
|
+
const url2 = URL.createObjectURL(blob);
|
|
121034
|
+
const img = new Image;
|
|
121035
|
+
img.onload = () => {
|
|
121036
|
+
ctx.drawImage(img, 0, 0);
|
|
121037
|
+
URL.revokeObjectURL(url2);
|
|
121038
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
121039
|
+
done(dataUrl.replace(/^data:image\/png;base64,/, ""));
|
|
121040
|
+
};
|
|
121041
|
+
img.onerror = () => {
|
|
121042
|
+
URL.revokeObjectURL(url2);
|
|
121043
|
+
ctx.fillStyle = "#000000";
|
|
121044
|
+
ctx.font = "16px monospace";
|
|
121045
|
+
const text3 = document.body.innerText || "";
|
|
121046
|
+
const lines = text3.split(`
|
|
121047
|
+
`);
|
|
121048
|
+
for (let i = 0;i < lines.length && i < 40; i++) {
|
|
121049
|
+
ctx.fillText(lines[i], 10, 20 + i * 20);
|
|
121050
|
+
}
|
|
121051
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
121052
|
+
done(dataUrl.replace(/^data:image\/png;base64,/, ""));
|
|
121053
|
+
};
|
|
121054
|
+
img.src = url2;
|
|
121055
|
+
setTimeout(() => {
|
|
121056
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
121057
|
+
done(dataUrl.replace(/^data:image\/png;base64,/, ""));
|
|
121058
|
+
}, 4000);
|
|
121059
|
+
} catch (e) {
|
|
121060
|
+
done("");
|
|
121061
|
+
}
|
|
121062
|
+
});
|
|
121063
|
+
if (!base64) {
|
|
121064
|
+
throw new Error("Canvas screenshot capture returned empty");
|
|
121065
|
+
}
|
|
121066
|
+
return base64;
|
|
121067
|
+
}
|
|
120996
121068
|
|
|
120997
121069
|
// src/commands/snapshot.ts
|
|
120998
121070
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
@@ -121250,21 +121322,17 @@ async function waitFor(selector, options = {}) {
|
|
|
121250
121322
|
// src/commands/eval.ts
|
|
121251
121323
|
async function evaluate(script) {
|
|
121252
121324
|
const browser3 = requireBrowser();
|
|
121253
|
-
const wrappedScript = `
|
|
121254
|
-
return (function() {
|
|
121255
|
-
${script}
|
|
121256
|
-
})();
|
|
121257
|
-
`;
|
|
121258
121325
|
try {
|
|
121326
|
+
const result = await browser3.execute(`return ${script}`);
|
|
121327
|
+
return result;
|
|
121328
|
+
} catch {
|
|
121329
|
+
const wrappedScript = `
|
|
121330
|
+
return (function() {
|
|
121331
|
+
${script}
|
|
121332
|
+
})();
|
|
121333
|
+
`;
|
|
121259
121334
|
const result = await browser3.execute(wrappedScript);
|
|
121260
121335
|
return result;
|
|
121261
|
-
} catch (err) {
|
|
121262
|
-
try {
|
|
121263
|
-
const result = await browser3.execute(`return ${script}`);
|
|
121264
|
-
return result;
|
|
121265
|
-
} catch {
|
|
121266
|
-
throw err;
|
|
121267
|
-
}
|
|
121268
121336
|
}
|
|
121269
121337
|
}
|
|
121270
121338
|
|
|
@@ -121278,7 +121346,7 @@ var HTML2CANVAS_CDN2 = "https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html
|
|
|
121278
121346
|
async function screenshot2(options = {}) {
|
|
121279
121347
|
const browser3 = requireBrowser();
|
|
121280
121348
|
const autoWait = options.autoWait ?? true;
|
|
121281
|
-
const timeout = options.timeout ??
|
|
121349
|
+
const timeout = options.timeout ?? 15000;
|
|
121282
121350
|
if (autoWait) {
|
|
121283
121351
|
await waitForDomStable();
|
|
121284
121352
|
}
|
|
@@ -121289,12 +121357,21 @@ async function screenshot2(options = {}) {
|
|
|
121289
121357
|
data2 = await withTimeout2(captureWithHtml2Canvas2(browser3), timeout, "html2canvas timed out");
|
|
121290
121358
|
method = "html2canvas";
|
|
121291
121359
|
} catch (err) {
|
|
121292
|
-
console.error(`html2canvas failed: ${err}, trying
|
|
121360
|
+
console.error(`html2canvas failed: ${err}, trying canvas fallback...`);
|
|
121293
121361
|
try {
|
|
121294
|
-
data2 = await withTimeout2(browser3
|
|
121295
|
-
method = "
|
|
121296
|
-
} catch (
|
|
121297
|
-
|
|
121362
|
+
data2 = await withTimeout2(captureWithCanvas2(browser3), timeout, "Canvas screenshot timed out");
|
|
121363
|
+
method = "canvas";
|
|
121364
|
+
} catch (canvasErr) {
|
|
121365
|
+
if (getXvfbDisplay() !== null) {
|
|
121366
|
+
throw new Error(`All screenshot methods failed in Xvfb: html2canvas: ${err}, canvas: ${canvasErr}`);
|
|
121367
|
+
}
|
|
121368
|
+
console.error(`Canvas failed: ${canvasErr}, trying native...`);
|
|
121369
|
+
try {
|
|
121370
|
+
data2 = await withTimeout2(browser3.takeScreenshot(), timeout, `Native screenshot timed out after ${timeout}ms`);
|
|
121371
|
+
method = "native";
|
|
121372
|
+
} catch (nativeErr) {
|
|
121373
|
+
throw new Error(`All screenshot methods failed: html2canvas: ${err}, canvas: ${canvasErr}, native: ${nativeErr}`);
|
|
121374
|
+
}
|
|
121298
121375
|
}
|
|
121299
121376
|
}
|
|
121300
121377
|
if (options.output) {
|
|
@@ -121361,6 +121438,61 @@ async function captureWithHtml2Canvas2(browser3) {
|
|
|
121361
121438
|
}
|
|
121362
121439
|
return base64;
|
|
121363
121440
|
}
|
|
121441
|
+
async function captureWithCanvas2(browser3) {
|
|
121442
|
+
const base64 = await browser3.executeAsync((done) => {
|
|
121443
|
+
try {
|
|
121444
|
+
const w2 = window.innerWidth || 800;
|
|
121445
|
+
const h = window.innerHeight || 600;
|
|
121446
|
+
const canvas = document.createElement("canvas");
|
|
121447
|
+
canvas.width = w2;
|
|
121448
|
+
canvas.height = h;
|
|
121449
|
+
const ctx = canvas.getContext("2d");
|
|
121450
|
+
ctx.fillStyle = "#ffffff";
|
|
121451
|
+
ctx.fillRect(0, 0, w2, h);
|
|
121452
|
+
const serializer = new XMLSerializer;
|
|
121453
|
+
const cloned = document.documentElement.cloneNode(true);
|
|
121454
|
+
const html3 = serializer.serializeToString(cloned);
|
|
121455
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w2}" height="${h}">
|
|
121456
|
+
<foreignObject width="100%" height="100%">
|
|
121457
|
+
${html3}
|
|
121458
|
+
</foreignObject>
|
|
121459
|
+
</svg>`;
|
|
121460
|
+
const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
|
|
121461
|
+
const url2 = URL.createObjectURL(blob);
|
|
121462
|
+
const img = new Image;
|
|
121463
|
+
img.onload = () => {
|
|
121464
|
+
ctx.drawImage(img, 0, 0);
|
|
121465
|
+
URL.revokeObjectURL(url2);
|
|
121466
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
121467
|
+
done(dataUrl.replace(/^data:image\/png;base64,/, ""));
|
|
121468
|
+
};
|
|
121469
|
+
img.onerror = () => {
|
|
121470
|
+
URL.revokeObjectURL(url2);
|
|
121471
|
+
ctx.fillStyle = "#000000";
|
|
121472
|
+
ctx.font = "16px monospace";
|
|
121473
|
+
const text3 = document.body.innerText || "";
|
|
121474
|
+
const lines = text3.split(`
|
|
121475
|
+
`);
|
|
121476
|
+
for (let i = 0;i < lines.length && i < 40; i++) {
|
|
121477
|
+
ctx.fillText(lines[i], 10, 20 + i * 20);
|
|
121478
|
+
}
|
|
121479
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
121480
|
+
done(dataUrl.replace(/^data:image\/png;base64,/, ""));
|
|
121481
|
+
};
|
|
121482
|
+
img.src = url2;
|
|
121483
|
+
setTimeout(() => {
|
|
121484
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
121485
|
+
done(dataUrl.replace(/^data:image\/png;base64,/, ""));
|
|
121486
|
+
}, 4000);
|
|
121487
|
+
} catch (e) {
|
|
121488
|
+
done("");
|
|
121489
|
+
}
|
|
121490
|
+
});
|
|
121491
|
+
if (!base64) {
|
|
121492
|
+
throw new Error("Canvas screenshot capture returned empty");
|
|
121493
|
+
}
|
|
121494
|
+
return base64;
|
|
121495
|
+
}
|
|
121364
121496
|
|
|
121365
121497
|
// src/commands/snapshot.ts
|
|
121366
121498
|
import { writeFile as writeFile4 } from "fs/promises";
|
|
@@ -121618,21 +121750,17 @@ async function waitFor2(selector, options = {}) {
|
|
|
121618
121750
|
// src/commands/eval.ts
|
|
121619
121751
|
async function evaluate2(script) {
|
|
121620
121752
|
const browser3 = requireBrowser();
|
|
121621
|
-
const wrappedScript = `
|
|
121622
|
-
return (function() {
|
|
121623
|
-
${script}
|
|
121624
|
-
})();
|
|
121625
|
-
`;
|
|
121626
121753
|
try {
|
|
121754
|
+
const result = await browser3.execute(`return ${script}`);
|
|
121755
|
+
return result;
|
|
121756
|
+
} catch {
|
|
121757
|
+
const wrappedScript = `
|
|
121758
|
+
return (function() {
|
|
121759
|
+
${script}
|
|
121760
|
+
})();
|
|
121761
|
+
`;
|
|
121627
121762
|
const result = await browser3.execute(wrappedScript);
|
|
121628
121763
|
return result;
|
|
121629
|
-
} catch (err) {
|
|
121630
|
-
try {
|
|
121631
|
-
const result = await browser3.execute(`return ${script}`);
|
|
121632
|
-
return result;
|
|
121633
|
-
} catch {
|
|
121634
|
-
throw err;
|
|
121635
|
-
}
|
|
121636
121764
|
}
|
|
121637
121765
|
}
|
|
121638
121766
|
|
|
@@ -121657,12 +121785,16 @@ async function executeCommand(cmd, globalAutoWait) {
|
|
|
121657
121785
|
console.error(`[${new Date().toISOString()}] activateWindow took ${Date.now() - activateStart}ms`);
|
|
121658
121786
|
const cmdStart = Date.now();
|
|
121659
121787
|
let result;
|
|
121788
|
+
if (cmd.cmd === "screenshot" || cmd.cmd === "snapshot") {
|
|
121789
|
+
await injectKeepAlive();
|
|
121790
|
+
}
|
|
121660
121791
|
switch (cmd.cmd) {
|
|
121661
121792
|
case "screenshot":
|
|
121662
121793
|
result = await screenshot2({
|
|
121663
121794
|
output: cmd.output,
|
|
121664
121795
|
fullPage: cmd.fullPage,
|
|
121665
|
-
autoWait
|
|
121796
|
+
autoWait,
|
|
121797
|
+
timeout: cmd.timeout
|
|
121666
121798
|
});
|
|
121667
121799
|
break;
|
|
121668
121800
|
case "snapshot":
|
|
@@ -122131,7 +122263,7 @@ async function waitForDisplay(display, timeoutMs = 1e4) {
|
|
|
122131
122263
|
return false;
|
|
122132
122264
|
}
|
|
122133
122265
|
var xvfbProcess = null;
|
|
122134
|
-
var
|
|
122266
|
+
var xvfbDisplay2 = null;
|
|
122135
122267
|
async function startXvfb() {
|
|
122136
122268
|
const display = findAvailableDisplay();
|
|
122137
122269
|
const displayStr = `:${display}`;
|
|
@@ -122161,7 +122293,7 @@ async function startXvfb() {
|
|
|
122161
122293
|
throw new Error(`Xvfb display ${displayStr} failed to start within timeout`);
|
|
122162
122294
|
}
|
|
122163
122295
|
console.error(`Xvfb ready on display ${displayStr}`);
|
|
122164
|
-
|
|
122296
|
+
xvfbDisplay2 = display;
|
|
122165
122297
|
process.env.DISPLAY = displayStr;
|
|
122166
122298
|
delete process.env.WAYLAND_DISPLAY;
|
|
122167
122299
|
process.env.GDK_BACKEND = "x11";
|
|
@@ -122172,10 +122304,250 @@ function stopXvfb() {
|
|
|
122172
122304
|
console.error("Stopping Xvfb...");
|
|
122173
122305
|
xvfbProcess.kill("SIGTERM");
|
|
122174
122306
|
xvfbProcess = null;
|
|
122175
|
-
|
|
122307
|
+
xvfbDisplay2 = null;
|
|
122176
122308
|
}
|
|
122177
122309
|
}
|
|
122178
122310
|
|
|
122311
|
+
// src/commands/install-skill.ts
|
|
122312
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
122313
|
+
import { resolve, dirname, join as join3 } from "path";
|
|
122314
|
+
import { homedir as homedir3 } from "os";
|
|
122315
|
+
|
|
122316
|
+
// src/generated/skill-content.ts
|
|
122317
|
+
var SKILL_MD_CONTENT = `---
|
|
122318
|
+
name: tauri-test-cli
|
|
122319
|
+
description: Use when needing to visually verify Tauri app behavior, test UI interactions, take screenshots, or inspect DOM state.
|
|
122320
|
+
---
|
|
122321
|
+
|
|
122322
|
+
# tauri-test-cli
|
|
122323
|
+
|
|
122324
|
+
Visual testing CLI for Tauri apps. Start server once, send commands anytime.
|
|
122325
|
+
|
|
122326
|
+
## Step 1: Detect Runtime & Set Command Prefix
|
|
122327
|
+
|
|
122328
|
+
> **Local dependency?** If \`node_modules/tauri-test-cli\` exists in the project, skip Steps 1-2 and use \`npx tauri-test-cli\` (or \`pnpm exec tauri-test-cli\` / \`bunx tauri-test-cli\`) as your command prefix.
|
|
122329
|
+
|
|
122330
|
+
Determine how to invoke tauri-test-cli based on the project:
|
|
122331
|
+
|
|
122332
|
+
\`\`\`bash
|
|
122333
|
+
# Check what's available
|
|
122334
|
+
ls pixi.toml bun.lock package-lock.json pnpm-lock.yaml 2>/dev/null
|
|
122335
|
+
\`\`\`
|
|
122336
|
+
|
|
122337
|
+
Set your command prefix for ALL subsequent commands:
|
|
122338
|
+
- **pixi + bun** (pixi.toml + bun.lock): \`pixi run bunx tauri-test-cli\`
|
|
122339
|
+
- **pixi + npm** (pixi.toml, no bun): \`pixi run npx tauri-test-cli\`
|
|
122340
|
+
- **bun** (bun.lock, no pixi): \`bunx tauri-test-cli\`
|
|
122341
|
+
- **npm** (package-lock.json): \`npx tauri-test-cli\`
|
|
122342
|
+
- **pnpm** (pnpm-lock.yaml): \`pnpm dlx tauri-test-cli\`
|
|
122343
|
+
|
|
122344
|
+
Use this prefix for EVERY command below. Examples below use \`CMD\` as placeholder.
|
|
122345
|
+
|
|
122346
|
+
## Step 2: Ensure Latest Version
|
|
122347
|
+
|
|
122348
|
+
**bunx and npx cache aggressively.** Always clear all caches before running:
|
|
122349
|
+
|
|
122350
|
+
\`\`\`bash
|
|
122351
|
+
# Clear ALL known cache locations
|
|
122352
|
+
rm -rf /tmp/bunx-*tauri-test-cli* 2>/dev/null # bunx temp cache (primary)
|
|
122353
|
+
rm -rf ~/.bun/install/cache/tauri-test-cli* 2>/dev/null # bun install cache
|
|
122354
|
+
rm -rf ~/.npm/_npx/*/node_modules/tauri-test-cli 2>/dev/null # npx cache
|
|
122355
|
+
\`\`\`
|
|
122356
|
+
|
|
122357
|
+
This ensures you always get the latest published version.
|
|
122358
|
+
|
|
122359
|
+
## Step 3: Check Dependencies
|
|
122360
|
+
|
|
122361
|
+
\`\`\`bash
|
|
122362
|
+
CMD check-deps
|
|
122363
|
+
\`\`\`
|
|
122364
|
+
|
|
122365
|
+
If tauri-driver is missing:
|
|
122366
|
+
\`\`\`bash
|
|
122367
|
+
CMD setup
|
|
122368
|
+
\`\`\`
|
|
122369
|
+
|
|
122370
|
+
## Step 4: Find the App Binary
|
|
122371
|
+
|
|
122372
|
+
\`\`\`bash
|
|
122373
|
+
# Search for tauri.conf.json to find the app name
|
|
122374
|
+
find . -name "tauri.conf.json" -not -path "*/node_modules/*" 2>/dev/null
|
|
122375
|
+
|
|
122376
|
+
# Common binary locations:
|
|
122377
|
+
# ./target/debug/app-name
|
|
122378
|
+
# ./src-tauri/target/debug/app-name
|
|
122379
|
+
# ./apps/*/src-tauri/target/debug/app-name
|
|
122380
|
+
\`\`\`
|
|
122381
|
+
|
|
122382
|
+
Read \`tauri.conf.json\` to get the app identifier/name. Check if the binary exists:
|
|
122383
|
+
\`\`\`bash
|
|
122384
|
+
ls ./path/to/target/debug/app-name 2>/dev/null
|
|
122385
|
+
\`\`\`
|
|
122386
|
+
|
|
122387
|
+
If missing, build it:
|
|
122388
|
+
\`\`\`bash
|
|
122389
|
+
# If pixi project:
|
|
122390
|
+
pixi run cargo build --manifest-path ./path/to/src-tauri/Cargo.toml
|
|
122391
|
+
# Otherwise:
|
|
122392
|
+
cd ./path/to/src-tauri && cargo build
|
|
122393
|
+
\`\`\`
|
|
122394
|
+
|
|
122395
|
+
## Step 5: Check if Dev Server Needed
|
|
122396
|
+
|
|
122397
|
+
Read \`tauri.conf.json\` — if it has a \`devUrl\` (e.g., \`http://localhost:5173\`), the debug binary needs the frontend dev server running.
|
|
122398
|
+
|
|
122399
|
+
\`\`\`bash
|
|
122400
|
+
# 1. Check if devUrl exists
|
|
122401
|
+
grep -o '"devUrl".*' path/to/tauri.conf.json
|
|
122402
|
+
|
|
122403
|
+
# 2. If devUrl found, check if the dev server is already reachable
|
|
122404
|
+
curl -s -o /dev/null -w "%{http_code}" http://localhost:5173
|
|
122405
|
+
|
|
122406
|
+
# 3. If curl fails (connection refused / non-200), start the dev server
|
|
122407
|
+
# Look at package.json for the right dev command
|
|
122408
|
+
cd frontend-dir && npm run dev &
|
|
122409
|
+
sleep 5
|
|
122410
|
+
|
|
122411
|
+
# 4. Verify it's reachable now
|
|
122412
|
+
curl -s -o /dev/null -w "%{http_code}" http://localhost:5173
|
|
122413
|
+
\`\`\`
|
|
122414
|
+
|
|
122415
|
+
If \`build/\` or \`dist/\` exists in the frontend directory, the binary may work without a dev server — but verify by checking the app loads correctly after starting the server.
|
|
122416
|
+
|
|
122417
|
+
## Step 6: Clean Up & Start Server
|
|
122418
|
+
|
|
122419
|
+
**IMPORTANT on Linux:** Always use \`--xvfb\` flag. This avoids window focus and rendering issues.
|
|
122420
|
+
|
|
122421
|
+
\`\`\`bash
|
|
122422
|
+
# Clean up any stale processes first
|
|
122423
|
+
CMD cleanup
|
|
122424
|
+
|
|
122425
|
+
# Start server (--xvfb on Linux, omit on macOS)
|
|
122426
|
+
CMD server --app ./path/to/binary --xvfb &
|
|
122427
|
+
|
|
122428
|
+
# Wait for server — it can take 15-20 seconds
|
|
122429
|
+
sleep 15
|
|
122430
|
+
CMD status
|
|
122431
|
+
\`\`\`
|
|
122432
|
+
|
|
122433
|
+
If status says "No server running", wait longer:
|
|
122434
|
+
\`\`\`bash
|
|
122435
|
+
sleep 10
|
|
122436
|
+
CMD status
|
|
122437
|
+
\`\`\`
|
|
122438
|
+
|
|
122439
|
+
**DO NOT** try to manually start Xvfb, use the existing Wayland display, or work around \`--xvfb\` failures. If \`--xvfb\` fails, check the error message and report it.
|
|
122440
|
+
|
|
122441
|
+
## Quick Reference
|
|
122442
|
+
|
|
122443
|
+
| Action | Command |
|
|
122444
|
+
|--------|---------|
|
|
122445
|
+
| Check deps | \`CMD check-deps\` |
|
|
122446
|
+
| Status | \`CMD status\` |
|
|
122447
|
+
| Start server | \`CMD server --app ./path/to/app --xvfb &\` |
|
|
122448
|
+
| Click | \`CMD click "selector"\` |
|
|
122449
|
+
| Type | \`CMD type "selector" "text"\` |
|
|
122450
|
+
| Screenshot | \`CMD screenshot --output /tmp/screen.png\` |
|
|
122451
|
+
| DOM snapshot | \`CMD snapshot --output /tmp/dom.yaml\` |
|
|
122452
|
+
| Wait appear | \`CMD wait "selector" --timeout 3000\` |
|
|
122453
|
+
| Wait gone | \`CMD wait "selector" --gone --timeout 5000\` |
|
|
122454
|
+
| Eval JS | \`CMD eval "document.title"\` |
|
|
122455
|
+
| Stop | \`CMD stop\` |
|
|
122456
|
+
| Cleanup | \`CMD cleanup\` |
|
|
122457
|
+
|
|
122458
|
+
## After Server Starts
|
|
122459
|
+
|
|
122460
|
+
\`\`\`bash
|
|
122461
|
+
# Always take an initial screenshot to verify the app loaded
|
|
122462
|
+
CMD screenshot --output /tmp/initial.png
|
|
122463
|
+
# Use Read tool to view the screenshot!
|
|
122464
|
+
|
|
122465
|
+
# Run test commands
|
|
122466
|
+
CMD click "button"
|
|
122467
|
+
CMD screenshot --output /tmp/after-click.png
|
|
122468
|
+
|
|
122469
|
+
# When done
|
|
122470
|
+
CMD stop
|
|
122471
|
+
\`\`\`
|
|
122472
|
+
|
|
122473
|
+
## Troubleshooting
|
|
122474
|
+
|
|
122475
|
+
| Error | Cause | Fix |
|
|
122476
|
+
|-------|-------|-----|
|
|
122477
|
+
| "Connection refused" in screenshot | Frontend dev server not running | Start frontend: \`npm run dev\` |
|
|
122478
|
+
| Blank/white screen | App not loaded yet | Increase \`--wait\` timeout |
|
|
122479
|
+
| "tauri-driver not found" | Missing dependency | Run \`CMD setup\` |
|
|
122480
|
+
| "Maximum sessions" error | Stale processes | Run \`CMD cleanup\` |
|
|
122481
|
+
| "Xvfb display failed to start" | Old CLI version (< 0.6.2) | Clear caches: \`rm -rf /tmp/bunx-*tauri-test-cli* ~/.bun/install/cache/tauri-test-cli*\` and retry |
|
|
122482
|
+
| Server not ready after 15s | Slow app startup | Wait longer, check if dev server is needed |
|
|
122483
|
+
|
|
122484
|
+
## Screenshot vs Snapshot
|
|
122485
|
+
|
|
122486
|
+
- **Screenshot**: Visual appearance, layout, colors
|
|
122487
|
+
- **DOM snapshot**: Element existence, text content, attributes
|
|
122488
|
+
- **Use snapshot** for canvas elements (screenshots can't capture canvas)
|
|
122489
|
+
|
|
122490
|
+
## Cleanup (if stuck)
|
|
122491
|
+
|
|
122492
|
+
\`\`\`bash
|
|
122493
|
+
CMD stop
|
|
122494
|
+
CMD cleanup
|
|
122495
|
+
pkill Xvfb 2>/dev/null # Linux only
|
|
122496
|
+
\`\`\`
|
|
122497
|
+
`;
|
|
122498
|
+
|
|
122499
|
+
// src/commands/install-skill.ts
|
|
122500
|
+
function findProjectRoot(cwd) {
|
|
122501
|
+
let dir = resolve(cwd);
|
|
122502
|
+
const root2 = dirname(dir) === dir ? dir : undefined;
|
|
122503
|
+
while (true) {
|
|
122504
|
+
if (existsSync(join3(dir, ".claude"))) {
|
|
122505
|
+
return dir;
|
|
122506
|
+
}
|
|
122507
|
+
const parent2 = dirname(dir);
|
|
122508
|
+
if (parent2 === dir)
|
|
122509
|
+
break;
|
|
122510
|
+
dir = parent2;
|
|
122511
|
+
}
|
|
122512
|
+
return null;
|
|
122513
|
+
}
|
|
122514
|
+
function installSkill(opts = {}) {
|
|
122515
|
+
let targetDir;
|
|
122516
|
+
let mode;
|
|
122517
|
+
if (opts.global && opts.project) {
|
|
122518
|
+
console.error("Error: cannot specify both --global and --project");
|
|
122519
|
+
return { success: false };
|
|
122520
|
+
}
|
|
122521
|
+
if (opts.global) {
|
|
122522
|
+
targetDir = join3(homedir3(), ".claude", "skills", "tauri-test-cli");
|
|
122523
|
+
mode = "global";
|
|
122524
|
+
} else if (opts.project) {
|
|
122525
|
+
targetDir = join3(process.cwd(), ".claude", "skills", "tauri-test-cli");
|
|
122526
|
+
mode = "project";
|
|
122527
|
+
} else {
|
|
122528
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
122529
|
+
if (projectRoot) {
|
|
122530
|
+
targetDir = join3(projectRoot, ".claude", "skills", "tauri-test-cli");
|
|
122531
|
+
mode = `project (${projectRoot})`;
|
|
122532
|
+
} else {
|
|
122533
|
+
targetDir = join3(homedir3(), ".claude", "skills", "tauri-test-cli");
|
|
122534
|
+
mode = "global";
|
|
122535
|
+
}
|
|
122536
|
+
}
|
|
122537
|
+
const targetPath = join3(targetDir, "SKILL.md");
|
|
122538
|
+
if (existsSync(targetPath)) {
|
|
122539
|
+
const existing = readFileSync2(targetPath, "utf-8");
|
|
122540
|
+
if (existing === SKILL_MD_CONTENT) {
|
|
122541
|
+
console.log(`Skill already up-to-date (${mode}): ${targetPath}`);
|
|
122542
|
+
return { success: true };
|
|
122543
|
+
}
|
|
122544
|
+
}
|
|
122545
|
+
mkdirSync(targetDir, { recursive: true });
|
|
122546
|
+
writeFileSync(targetPath, SKILL_MD_CONTENT, "utf-8");
|
|
122547
|
+
console.log(`Installed skill (${mode}): ${targetPath}`);
|
|
122548
|
+
return { success: true };
|
|
122549
|
+
}
|
|
122550
|
+
|
|
122179
122551
|
// src/cli.ts
|
|
122180
122552
|
async function sendToServer(port, cmd) {
|
|
122181
122553
|
const url2 = `http://127.0.0.1:${port}`;
|
|
@@ -122256,6 +122628,9 @@ COMMANDS:
|
|
|
122256
122628
|
check-deps
|
|
122257
122629
|
Check if all required system dependencies are installed
|
|
122258
122630
|
|
|
122631
|
+
install-skill [--global | --project]
|
|
122632
|
+
Install Claude Code skill for tauri-test-cli
|
|
122633
|
+
|
|
122259
122634
|
help
|
|
122260
122635
|
Show this help message
|
|
122261
122636
|
|
|
@@ -122375,7 +122750,8 @@ async function executeCommand2(cmd, globalAutoWait) {
|
|
|
122375
122750
|
return await screenshot({
|
|
122376
122751
|
output: cmd.output,
|
|
122377
122752
|
fullPage: cmd.fullPage,
|
|
122378
|
-
autoWait
|
|
122753
|
+
autoWait,
|
|
122754
|
+
timeout: cmd.timeout
|
|
122379
122755
|
});
|
|
122380
122756
|
case "snapshot":
|
|
122381
122757
|
return await snapshot({
|
|
@@ -122412,7 +122788,7 @@ async function executeCommand2(cmd, globalAutoWait) {
|
|
|
122412
122788
|
}
|
|
122413
122789
|
return await evaluate(cmd.script);
|
|
122414
122790
|
case "sleep":
|
|
122415
|
-
await new Promise((
|
|
122791
|
+
await new Promise((resolve3) => setTimeout(resolve3, cmd.ms ?? 1000));
|
|
122416
122792
|
return { slept: cmd.ms ?? 1000 };
|
|
122417
122793
|
default:
|
|
122418
122794
|
throw new Error(`Unknown command: ${cmd.cmd}`);
|
|
@@ -122484,7 +122860,14 @@ async function main() {
|
|
|
122484
122860
|
process.exit(1);
|
|
122485
122861
|
}
|
|
122486
122862
|
}
|
|
122487
|
-
|
|
122863
|
+
if (command === "install-skill") {
|
|
122864
|
+
const result = installSkill({
|
|
122865
|
+
global: !!options.global,
|
|
122866
|
+
project: !!options.project
|
|
122867
|
+
});
|
|
122868
|
+
process.exit(result.success ? 0 : 1);
|
|
122869
|
+
}
|
|
122870
|
+
const appPath = options.app ? resolve2(options.app) : undefined;
|
|
122488
122871
|
const jsonOutput = !!options.json;
|
|
122489
122872
|
const port = options.port ? parseInt(options.port) : 9222;
|
|
122490
122873
|
const clientModeCommands = ["screenshot", "snapshot", "click", "type", "wait", "eval"];
|
|
@@ -122507,7 +122890,8 @@ async function main() {
|
|
|
122507
122890
|
cmd = {
|
|
122508
122891
|
cmd: "screenshot",
|
|
122509
122892
|
output: options.output,
|
|
122510
|
-
fullPage: !!options["full-page"]
|
|
122893
|
+
fullPage: !!options["full-page"],
|
|
122894
|
+
timeout: options.timeout ? parseInt(options.timeout) : undefined
|
|
122511
122895
|
};
|
|
122512
122896
|
break;
|
|
122513
122897
|
case "snapshot":
|
|
@@ -122666,7 +123050,8 @@ async function main() {
|
|
|
122666
123050
|
result = await screenshot({
|
|
122667
123051
|
output: options.output,
|
|
122668
123052
|
fullPage: !!options["full-page"],
|
|
122669
|
-
autoWait
|
|
123053
|
+
autoWait,
|
|
123054
|
+
timeout: options.timeout ? parseInt(options.timeout) : undefined
|
|
122670
123055
|
});
|
|
122671
123056
|
break;
|
|
122672
123057
|
case "snapshot":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tauri-test-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "CLI for testing Tauri applications with screenshot capture, DOM inspection, and user interaction simulation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"typecheck": "tsc --noEmit",
|
|
31
31
|
"test": "bun test",
|
|
32
32
|
"test:watch": "bun test --watch",
|
|
33
|
-
"
|
|
33
|
+
"generate-skill-content": "bun scripts/generate-skill-content.ts",
|
|
34
|
+
"prepublishOnly": "bun run generate-skill-content && bun run build && bun test",
|
|
34
35
|
"postinstall": "node scripts/postinstall.js"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Reads skills/tauri-test-cli/SKILL.md and generates src/generated/skill-content.ts
|
|
4
|
+
* with the content embedded as a string constant for bundling.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
7
|
+
import { resolve, dirname } from "path";
|
|
8
|
+
|
|
9
|
+
const ROOT = resolve(dirname(new URL(import.meta.url).pathname), "..");
|
|
10
|
+
const SKILL_PATH = resolve(ROOT, "skills/tauri-test-cli/SKILL.md");
|
|
11
|
+
const OUT_DIR = resolve(ROOT, "src/generated");
|
|
12
|
+
const OUT_PATH = resolve(OUT_DIR, "skill-content.ts");
|
|
13
|
+
|
|
14
|
+
const raw = readFileSync(SKILL_PATH, "utf-8");
|
|
15
|
+
|
|
16
|
+
// Escape for template literal embedding
|
|
17
|
+
const escaped = raw
|
|
18
|
+
.replace(/\\/g, "\\\\")
|
|
19
|
+
.replace(/`/g, "\\`")
|
|
20
|
+
.replace(/\$/g, "\\$");
|
|
21
|
+
|
|
22
|
+
const output = `// AUTO-GENERATED — do not edit. Run: bun scripts/generate-skill-content.ts
|
|
23
|
+
export const SKILL_MD_CONTENT = \`${escaped}\`;
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
mkdirSync(OUT_DIR, { recursive: true });
|
|
27
|
+
writeFileSync(OUT_PATH, output, "utf-8");
|
|
28
|
+
console.log(`Generated ${OUT_PATH}`);
|