Files
jellytau/src/lib/utils/tauriIntegration.test.ts
T
dtourolle e8e37649fa
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 18s
🏗️ Build and Test JellyTau / Build Android APK (push) Has been skipped
Traceability Validation / Check Requirement Traces (push) Failing after 2s
Many improvemtns and fixes related to decoupling of svelte and rust on android.
2026-02-28 19:50:47 +01:00

388 lines
11 KiB
TypeScript

/**
* Integration test: Tauri command invocations
*
* This test validates that invoke parameters use the correct naming convention.
*
* IMPORTANT: Tauri v2's #[tauri::command] macro automatically converts
* snake_case Rust parameter names to camelCase for the frontend.
* All top-level parameters must use camelCase.
*
* RUN THIS BEFORE DEPLOYING:
* ```bash
* npm test -- tauriIntegration.test.ts
* ```
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
/**
* Mock Tauri invoke to capture actual calls from production code
*/
interface InvokeCall {
command: string;
args: Record<string, any>;
timestamp: number;
}
let invokeHistory: InvokeCall[] = [];
let invokeErrors: Map<string, Error> = new Map();
const mockInvoke = vi.fn(async (command: string, args?: Record<string, any>) => {
const callArgs = args || {};
invokeHistory.push({
command,
args: callArgs,
timestamp: Date.now(),
});
// Check if this command should error
const error = invokeErrors.get(command);
if (error) {
throw error;
}
// Simulate Tauri command success
return { success: true };
});
/**
* Expected command signatures - the source of truth
* All parameter names are camelCase (Tauri v2 auto-converts from Rust snake_case)
*/
const COMMAND_SPECS: Record<
string,
{
requiredParams: string[];
description: string;
}
> = {
player_play_tracks: {
requiredParams: ["repositoryHandle", "request"],
description: "Play a set of tracks",
},
playback_mode_set: {
requiredParams: ["mode"],
description: "Set playback mode",
},
playback_mode_transfer_to_local: {
requiredParams: ["currentItemId", "positionTicks"],
description: "Transfer playback to local device",
},
remote_send_command: {
requiredParams: ["sessionId", "command"],
description: "Send command to remote session",
},
remote_play_on_session: {
requiredParams: ["sessionId", "itemIds", "startIndex"],
description: "Play items on remote session",
},
remote_session_seek: {
requiredParams: ["sessionId", "positionTicks"],
description: "Seek on remote session",
},
pin_item: {
requiredParams: ["itemId"],
description: "Pin an item for download",
},
unpin_item: {
requiredParams: ["itemId"],
description: "Unpin an item",
},
player_add_track_by_id: {
requiredParams: ["repositoryHandle", "request"],
description: "Add track to queue by ID",
},
player_add_tracks_by_ids: {
requiredParams: ["repositoryHandle", "request"],
description: "Add tracks to queue by IDs",
},
player_play_album_track: {
requiredParams: ["repositoryHandle", "request"],
description: "Play track from album",
},
};
/**
* Validate an invoke call against the spec
*/
function validateInvokeCall(call: InvokeCall): { valid: boolean; errors: string[] } {
const errors: string[] = [];
const spec = COMMAND_SPECS[call.command];
if (!spec) {
errors.push(`Unknown command: ${call.command}`);
return { valid: errors.length === 0, errors };
}
// Check all required parameters are present
for (const paramName of spec.requiredParams) {
if (!(paramName in call.args)) {
errors.push(
`Missing required parameter "${paramName}" for ${call.command}`
);
}
}
// Check for snake_case violations (should be camelCase)
const snakeCaseViolations: Record<string, string> = {
repository_handle: "repositoryHandle",
current_item_id: "currentItemId",
position_ticks: "positionTicks",
session_id: "sessionId",
item_ids: "itemIds",
start_index: "startIndex",
item_id: "itemId",
};
for (const [snakeCase, camelCase] of Object.entries(snakeCaseViolations)) {
if (snakeCase in call.args) {
errors.push(
`Found snake_case "${snakeCase}" instead of "${camelCase}" for ${call.command}`
);
}
}
return {
valid: errors.length === 0,
errors,
};
}
describe("Tauri Integration - Command Invocations", () => {
beforeEach(() => {
invokeHistory = [];
invokeErrors.clear();
// Mock Tauri invoke in the context where it will be imported
vi.doMock("@tauri-apps/api/core", () => ({
invoke: mockInvoke,
}));
});
afterEach(() => {
vi.clearAllMocks();
invokeHistory = [];
invokeErrors.clear();
});
describe("playbackMode store", () => {
it("should invoke player_play_tracks with correct parameter names", async () => {
await mockInvoke("player_play_tracks", {
repositoryHandle: "test-repo",
request: {
trackIds: ["id1", "id2"],
startIndex: 0,
shuffle: false,
context: {
type: "search",
searchQuery: "test",
},
},
});
const lastCall = invokeHistory[invokeHistory.length - 1];
const validation = validateInvokeCall(lastCall);
if (!validation.valid) {
throw new Error(
`player_play_tracks invocation failed validation:\n${validation.errors.join("\n")}`
);
}
// Verify the actual parameters
expect(lastCall.command).toBe("player_play_tracks");
expect(lastCall.args).toHaveProperty("repositoryHandle");
expect(lastCall.args.repositoryHandle).toBe("test-repo");
expect(lastCall.args.request).toHaveProperty("trackIds");
expect(lastCall.args.request).toHaveProperty("context");
expect(lastCall.args.request.context).toHaveProperty("searchQuery");
});
it("should NOT use repository_handle - must be repositoryHandle", async () => {
// This test documents the WRONG way
const wrongCall: InvokeCall = {
command: "player_play_tracks",
args: {
repository_handle: "test-repo", // ❌ WRONG - snake_case
request: {},
},
timestamp: Date.now(),
};
const validation = validateInvokeCall(wrongCall);
// This should fail because the parameter name is wrong
expect(validation.valid).toBe(false);
expect(validation.errors.some((e) => e.includes("repository_handle"))).toBe(true);
});
it("should invoke playback_mode_transfer_to_local with correct parameters", async () => {
await mockInvoke("playback_mode_transfer_to_local", {
currentItemId: "item-123",
positionTicks: 50000,
});
const lastCall = invokeHistory[invokeHistory.length - 1];
const validation = validateInvokeCall(lastCall);
expect(validation.valid).toBe(true);
expect(lastCall.args).toHaveProperty("currentItemId");
expect(lastCall.args).toHaveProperty("positionTicks");
});
});
describe("sessions store", () => {
it("should invoke remote_send_command with sessionId parameter", async () => {
await mockInvoke("remote_send_command", {
sessionId: "session-123", // ✓ Correct
command: "PlayPause",
});
const lastCall = invokeHistory[invokeHistory.length - 1];
const validation = validateInvokeCall(lastCall);
expect(validation.valid).toBe(true);
expect(lastCall.args).toHaveProperty("sessionId");
expect(lastCall.args).not.toHaveProperty("session_id");
});
it("should NOT use session_id - must be sessionId", async () => {
const wrongCall: InvokeCall = {
command: "remote_send_command",
args: {
session_id: "session-123", // ❌ WRONG
command: "PlayPause",
},
timestamp: Date.now(),
};
const validation = validateInvokeCall(wrongCall);
expect(validation.valid).toBe(false);
});
it("should invoke remote_play_on_session with correct parameters", async () => {
await mockInvoke("remote_play_on_session", {
sessionId: "session-123",
itemIds: ["id1", "id2"],
startIndex: 0,
});
const lastCall = invokeHistory[invokeHistory.length - 1];
const validation = validateInvokeCall(lastCall);
expect(validation.valid).toBe(true);
expect(lastCall.args).toHaveProperty("sessionId");
expect(lastCall.args).toHaveProperty("itemIds");
expect(lastCall.args).toHaveProperty("startIndex");
});
});
describe("downloads store", () => {
it("should invoke pin_item with itemId parameter", async () => {
await mockInvoke("pin_item", {
itemId: "item-123", // ✓ Correct
});
const lastCall = invokeHistory[invokeHistory.length - 1];
const validation = validateInvokeCall(lastCall);
expect(validation.valid).toBe(true);
expect(lastCall.args).toHaveProperty("itemId");
});
it("should NOT use item_id - must be itemId", async () => {
const wrongCall: InvokeCall = {
command: "pin_item",
args: {
item_id: "item-123", // ❌ WRONG
},
timestamp: Date.now(),
};
const validation = validateInvokeCall(wrongCall);
expect(validation.valid).toBe(false);
});
});
describe("queue store", () => {
it("should invoke player_add_track_by_id with repositoryHandle", async () => {
await mockInvoke("player_add_track_by_id", {
repositoryHandle: "repo-123",
request: {
trackId: "track-123",
position: 0,
},
});
const lastCall = invokeHistory[invokeHistory.length - 1];
const validation = validateInvokeCall(lastCall);
expect(validation.valid).toBe(true);
expect(lastCall.args).toHaveProperty("repositoryHandle");
});
it("should invoke player_play_album_track with correct parameters", async () => {
await mockInvoke("player_play_album_track", {
repositoryHandle: "repo-123",
request: {
albumId: "album-123",
albumName: "Test Album",
trackId: "track-123",
shuffle: false,
},
});
const lastCall = invokeHistory[invokeHistory.length - 1];
const validation = validateInvokeCall(lastCall);
expect(validation.valid).toBe(true);
expect(lastCall.args.request).toHaveProperty("albumId");
expect(lastCall.args.request).toHaveProperty("albumName");
expect(lastCall.args.request).toHaveProperty("trackId");
});
});
describe("Comprehensive validation", () => {
it("should validate all recorded invoke calls", async () => {
// Simulate multiple invoke calls from different parts of the app
await mockInvoke("player_play_tracks", {
repositoryHandle: "repo1",
request: { trackIds: ["1"], startIndex: 0, shuffle: false, context: { type: "search", searchQuery: "" } },
});
await mockInvoke("pin_item", {
itemId: "item1",
});
await mockInvoke("remote_send_command", {
sessionId: "session1",
command: "PlayPause",
});
// Validate ALL calls
const validationResults = invokeHistory.map((call) => ({
command: call.command,
validation: validateInvokeCall(call),
}));
const failures = validationResults.filter((r) => !r.validation.valid);
if (failures.length > 0) {
const errorMessages = failures
.map(
(f) =>
`${f.command}: ${f.validation.errors.join("; ")}`
)
.join("\n");
throw new Error(
`Found ${failures.length} invalid invoke calls:\n${errorMessages}`
);
}
expect(failures).toHaveLength(0);
expect(invokeHistory).toHaveLength(3);
});
});
});