mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-14 15:24:00 -04:00
460 lines
22 KiB
JavaScript
460 lines
22 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFileSync } from "node:child_process";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
import vm from "node:vm";
|
|
|
|
const read = path => readFileSync(new URL("../" + path, import.meta.url), "utf8");
|
|
|
|
// codecNameFromBluez reads BlueZ's Configuration byte array: the first four
|
|
// bytes are the vendor id, the next two the vendor codec id, both little endian.
|
|
function bluezConfiguration(vendor, codec) {
|
|
const bytes = [];
|
|
for (let i = 0; i < 4; i++)
|
|
bytes.push((vendor >>> (i * 8)) & 0xff);
|
|
bytes.push(codec & 0xff, (codec >>> 8) & 0xff);
|
|
return bytes;
|
|
}
|
|
|
|
const serviceSource = read("Services/BluetoothService.qml");
|
|
|
|
function methods(names) {
|
|
const bodies = names.map(name => {
|
|
const start = serviceSource.indexOf(" function " + name + "(");
|
|
assert.notEqual(start, -1, "BluetoothService.qml lost " + name);
|
|
const end = serviceSource.indexOf("\n }", start) + 6;
|
|
return serviceSource.slice(start, end);
|
|
});
|
|
const context = vm.createContext({ I18n: { tr: text => text } });
|
|
vm.runInContext(bodies.join("\n"), context);
|
|
return context;
|
|
}
|
|
|
|
const luaScript = new URL("../scripts/bluez-card-profile.lua", import.meta.url).pathname;
|
|
|
|
// list mode emits the profile index and the raw description; set mode matches a
|
|
// profile by index, by name, or by an untranslated codec literal passed from the
|
|
// UI. The script never parses the translated description, so word order and
|
|
// punctuation no longer matter.
|
|
const deviceDriver = String.raw`
|
|
local path = arg[1]
|
|
local src = assert(io.open(path)):read("*a")
|
|
local region = assert(src:match("(local function profile_available.-\nend\n)\nom:connect"),
|
|
"bluez-card-profile.lua: profile_available/handle_device region moved")
|
|
|
|
cutils = { parseParam = function(param) return param end }
|
|
Pod = { Object = function(table) return table end }
|
|
Core = { quit = function() end, sync = function(callback) if callback then callback() end end }
|
|
fail = function(message) print("FAIL\t" .. message) end
|
|
dev_name = "test-dev"
|
|
mode = "list"
|
|
target = ""
|
|
args = {}
|
|
handled = false
|
|
|
|
local handle_device = assert(load(region .. "\nreturn handle_device", "handle"))()
|
|
|
|
local function device_with(profiles, active, name)
|
|
return {
|
|
properties = { ["device.name"] = name or "test-dev" },
|
|
iterate_params = function(self, kind)
|
|
local list = profiles
|
|
if kind == "Profile" then
|
|
list = active and { { index = active } } or {}
|
|
end
|
|
local i = 0
|
|
return function()
|
|
i = i + 1
|
|
return list[i]
|
|
end
|
|
end,
|
|
set_param = function(self, kind, param)
|
|
print(string.format("SET\t%s\t%s", kind, tostring(param.index)))
|
|
end,
|
|
}
|
|
end
|
|
|
|
local function run_list(label, profiles, active)
|
|
mode = "list"
|
|
handled = false
|
|
target = ""
|
|
args = {}
|
|
print("CASE\t" .. label)
|
|
handle_device(device_with(profiles, active))
|
|
print("HANDLED\t" .. tostring(handled))
|
|
end
|
|
|
|
local function run_set(label, requested, requested_index, requested_codec, profiles)
|
|
mode = "set"
|
|
handled = false
|
|
target = requested
|
|
args = { index = requested_index, codec = requested_codec }
|
|
print("CASE\t" .. label)
|
|
handle_device(device_with(profiles))
|
|
print("HANDLED\t" .. tostring(handled))
|
|
end
|
|
|
|
run_list("list", {
|
|
{ name = "a2dp-sink", description = "高保真回放 (A2DP 信宿, 编码 LHDC v5)", index = 3 },
|
|
{ name = "a2dp-sink-sbc", description = "High Fidelity Playback (A2DP Sink, codec SBC)", index = 1 },
|
|
{ name = "off", description = "关", index = 0 },
|
|
}, 3)
|
|
|
|
run_set("set-index", "a2dp-sink", 5, "", {
|
|
{ name = "a2dp-sink", description = "高保真回放 (A2DP 信宿, 编码 LHDC v5)", index = 5 },
|
|
})
|
|
run_set("set-name", "a2dp-sink-sbc_xq", nil, "", {
|
|
{ name = "a2dp-sink-sbc_xq", description = "High Fidelity Playback (A2DP Sink, codec SBC-XQ)", index = 4 },
|
|
})
|
|
run_set("set-codec-fi", "", nil, "LHDC v5", {
|
|
{ name = "a2dp-sink", description = "Korkealaatuinen toisto (A2DP-kohde, LHDC v5-koodekki)", index = 7 },
|
|
})
|
|
run_set("set-codec-bg", "", nil, "LHDC v5", {
|
|
{ name = "a2dp-sink", description = "Изпълнение с висока точност (елемент-приемник A2DP, кодер „LHDC v5“)", index = 9 },
|
|
})
|
|
run_set("set-no-match", "", nil, "aptX", {
|
|
{ name = "a2dp-sink-sbc", description = "High Fidelity Playback (A2DP Sink, codec SBC)", index = 1 },
|
|
})
|
|
run_set("set-name-over-index", "a2dp-sink-sbc_xq", 1, "", {
|
|
{ name = "a2dp-sink-sbc", description = "High Fidelity Playback (A2DP Sink, codec SBC)", index = 1 },
|
|
{ name = "a2dp-sink-sbc_xq", description = "High Fidelity Playback (A2DP Sink, codec SBC-XQ)", index = 2 },
|
|
})
|
|
`;
|
|
|
|
function parseDeviceOutput(output) {
|
|
const cases = {};
|
|
let current = null;
|
|
for (const line of output.split("\n")) {
|
|
const columns = line.split("\t");
|
|
if (columns[0] === "CASE") {
|
|
current = cases[columns[1]] = { codecs: [], set: null, ok: null, fail: null, handled: null };
|
|
} else if (!current) {
|
|
continue;
|
|
} else if (columns[0] === "CODEC") {
|
|
current.codecs.push(columns.slice(1));
|
|
} else if (columns[0] === "SET") {
|
|
current.set = columns[1] + ":" + columns[2];
|
|
} else if (columns[0] === "OK") {
|
|
current.ok = columns[1];
|
|
} else if (columns[0] === "FAIL") {
|
|
current.fail = columns[1];
|
|
} else if (columns[0] === "HANDLED") {
|
|
current.handled = columns[1] === "true";
|
|
}
|
|
}
|
|
return cases;
|
|
}
|
|
|
|
test("list mode reports every available profile by index and raw description", () => {
|
|
const output = execFileSync("lua", ["-", luaScript], { input: deviceDriver, encoding: "utf8" });
|
|
const cases = parseDeviceOutput(output);
|
|
assert.deepEqual(cases.list.codecs, [
|
|
["3", "a2dp-sink", "高保真回放 (A2DP 信宿, 编码 LHDC v5)", "1"],
|
|
["1", "a2dp-sink-sbc", "High Fidelity Playback (A2DP Sink, codec SBC)", "0"],
|
|
["0", "off", "关", "0"],
|
|
]);
|
|
});
|
|
|
|
test("set mode matches a profile by index, name or untranslated codec literal", () => {
|
|
const output = execFileSync("lua", ["-", luaScript], { input: deviceDriver, encoding: "utf8" });
|
|
const cases = parseDeviceOutput(output);
|
|
assert.deepEqual(cases["set-index"], { codecs: [], set: "Profile:5", ok: "a2dp-sink", fail: null, handled: true });
|
|
assert.deepEqual(cases["set-name"], { codecs: [], set: "Profile:4", ok: "a2dp-sink-sbc_xq", fail: null, handled: true });
|
|
assert.deepEqual(cases["set-codec-fi"], { codecs: [], set: "Profile:7", ok: "a2dp-sink", fail: null, handled: true });
|
|
assert.deepEqual(cases["set-codec-bg"], { codecs: [], set: "Profile:9", ok: "a2dp-sink", fail: null, handled: true });
|
|
assert.deepEqual(cases["set-no-match"], { codecs: [], set: null, ok: null, fail: "profile not found: aptX", handled: true });
|
|
assert.deepEqual(cases["set-name-over-index"], { codecs: [], set: "Profile:2", ok: "a2dp-sink-sbc_xq", fail: null, handled: true });
|
|
});
|
|
|
|
test("a codec is identified from its description in any locale", () => {
|
|
const context = methods(["codecMap", "codecAliases", "codecInfoFromDescription", "getCodecInfo"]);
|
|
const info = description => context.codecInfoFromDescription(description);
|
|
assert.equal(info("高保真回放 (A2DP 信宿, 编码 LHDC v5)").name, "LHDC v5");
|
|
assert.equal(info("Korkealaatuinen toisto (A2DP-kohde, LHDC v5-koodekki)").name, "LHDC v5");
|
|
assert.equal(info("Magas hűségű lejátszás (A2DP fogadó, LHDC v5 kodek)").name, "LHDC v5");
|
|
assert.equal(info("Изпълнение с висока точност (елемент-приемник A2DP, кодер „LHDC v5“)").name, "LHDC v5");
|
|
assert.equal(info("မိုက်ပါနားကြပ်ခေါင်းယူနစ် (HSP/HFP၊ codec LHDC v5)").name, "LHDC v5");
|
|
assert.equal(info("High Fidelity Playback (A2DP Sink, codec SBC-XQ)").name, "SBC-XQ");
|
|
assert.equal(info("High Fidelity Playback (A2DP Sink, codec SBC)").name, "SBC");
|
|
assert.equal(info("Playback (A2DP Sink, codec aptX-LL)").name, "aptX LL");
|
|
// The exact PipeWire name wins over a shorter one inside it.
|
|
assert.equal(info("High Fidelity Playback (A2DP Sink, codec LC3plus HR)").name, "LC3plus HR");
|
|
assert.equal(info("Playback (A2DP Sink, codec aptX-LL mSBC)").name, "aptX LL");
|
|
assert.equal(info("Headset (HSP/HFP, codec MSBC)").name, "mSBC");
|
|
assert.equal(info("关"), null);
|
|
assert.equal(info("Auto: Prefer Quality (A2DP)"), null);
|
|
});
|
|
|
|
test("a CODEC line carries the profile index and the codec read from the description", () => {
|
|
const context = methods(["codecMap", "codecAliases", "codecInfoFromDescription", "codecLabelFromProfile", "getCodecInfo", "parseCodecLine"]);
|
|
const entry = context.parseCodecLine("CODEC\t3\ta2dp-sink\t高保真回放 (A2DP 信宿, 编码 LHDC v5)\t1");
|
|
assert.equal(entry.name, "LHDC v5");
|
|
assert.equal(entry.profile, "a2dp-sink");
|
|
assert.equal(entry.index, 3);
|
|
assert.equal(entry.codec, "LHDC v5");
|
|
assert.equal(entry.current, true);
|
|
assert.equal(context.parseCodecLine("CODEC\t0\toff\t关\t0"), null);
|
|
assert.equal(context.parseCodecLine("OK\ta2dp-sink"), null);
|
|
// A codec outside the vocabulary stays selectable under its profile name.
|
|
const unknown = context.parseCodecLine("CODEC\t9\ta2dp-sink-brandnew_x\tHigh Fidelity Playback (A2DP Sink, codec BrandNew X)\t0");
|
|
assert.equal(unknown.name, "BRANDNEW_X");
|
|
assert.equal(unknown.codec, "");
|
|
assert.equal(unknown.description, "Unknown codec");
|
|
const bareUnknown = context.parseCodecLine("CODEC\t3\ta2dp-sink\t高保真回放 (A2DP 信宿, 编码 未知编解码器)\t1");
|
|
assert.equal(bareUnknown.name, "Unknown");
|
|
assert.equal(bareUnknown.profile, "a2dp-sink");
|
|
assert.equal(bareUnknown.codec, "");
|
|
assert.equal(bareUnknown.current, true);
|
|
assert.equal(bareUnknown.unknown, true);
|
|
assert.equal(entry.unknown, false);
|
|
const headsetUnknown = context.parseCodecLine("CODEC\t4\theadset-head-unit\tHeadset (HSP/HFP)\t0");
|
|
assert.equal(headsetUnknown.name, "Unknown");
|
|
assert.equal(headsetUnknown.profile, "headset-head-unit");
|
|
});
|
|
|
|
test("an active unknown profile cannot claim a codec name before BlueZ lookup", () => {
|
|
const context = methods(["codecMap", "codecAliases", "codecInfoFromDescription", "codecLabelFromProfile", "getCodecInfo", "parseCodecLine", "codecCategory"]);
|
|
const process = serviceSource.split(" id: codecListProcess")[1].split(" Process {")[0];
|
|
const handlerSource = process.match(/onRead: (data => \{[\s\S]*?\n \})\n \}\n\n onExited/)?.[1];
|
|
assert.ok(handlerSource, "codecListProcess onRead handler moved");
|
|
const state = { detectedCodec: "", currentIndex: -1, availableCodecs: [] };
|
|
context.root = context;
|
|
context.codecListProcess = state;
|
|
const onRead = vm.runInContext(handlerSource, context);
|
|
onRead("CODEC\t3\ta2dp-sink\t高保真回放 (A2DP 信宿, 编码 未知编解码器)\t1");
|
|
assert.equal(state.detectedCodec, "");
|
|
assert.equal(state.currentIndex, 3);
|
|
assert.equal(state.availableCodecs[0].name, "Unknown");
|
|
assert.equal(state.availableCodecs[0].profile, "a2dp-sink");
|
|
onRead("CODEC\t4\theadset-head-unit\tHeadset (HSP/HFP)\t0");
|
|
assert.equal(state.availableCodecs[1].name, "Unknown");
|
|
assert.notEqual(state.availableCodecs[0].index, state.availableCodecs[1].index);
|
|
const exitSource = process.match(/onExited: (function \(exitCode\) \{[\s\S]*?\n \})\n \}/)?.[1];
|
|
assert.ok(exitSource, "codecListProcess onExited handler moved");
|
|
state.callback = (codecs, current, currentIndex) => {
|
|
assert.equal(codecs.length, 2);
|
|
assert.equal(current, "");
|
|
assert.equal(currentIndex, 3);
|
|
};
|
|
Object.assign(context, state);
|
|
vm.runInContext(`(${exitSource})`, context)(0);
|
|
assert.equal(context.currentIndex, -1);
|
|
});
|
|
|
|
test("an unknown active PipeWire profile preserves choices but defers current codec to BlueZ", () => {
|
|
const context = methods(["refreshDeviceCodec", "getAvailableCodecs", "invalidateCodecQuery", "codecQueryIsCurrent"]);
|
|
const device = { connected: true, address: "AA:BB" };
|
|
const pipewire = [{ name: "Unknown", profile: "a2dp-sink", index: 3 }];
|
|
const bluez = [{ name: "LHDC v5" }];
|
|
let bluezQueries = 0;
|
|
const updates = [];
|
|
context.isAudioDevice = () => true;
|
|
context.whenCodecBackendReady = callback => callback();
|
|
context.root = {
|
|
invalidateCodecQuery: context.invalidateCodecQuery,
|
|
codecQueryIsCurrent: context.codecQueryIsCurrent,
|
|
codecQueryVersions: {},
|
|
adapter: { devices: { values: [device] } },
|
|
wpexecAvailable: true,
|
|
dbusBridgeAvailable: true,
|
|
queryCardProfiles: (_device, callback) => callback(pipewire, "", 3),
|
|
queryBluezCodecState: (_device, callback) => { bluezQueries++; callback(bluez, "LHDC v5"); },
|
|
updateDeviceCodec: (_address, name) => updates.push(name)
|
|
};
|
|
context.refreshDeviceCodec(device);
|
|
assert.deepEqual(updates, ["LHDC v5"]);
|
|
context.getAvailableCodecs(device, (codecs, current, currentIndex) => {
|
|
assert.equal(codecs, pipewire);
|
|
assert.equal(current, "LHDC v5");
|
|
assert.equal(currentIndex, 3);
|
|
});
|
|
assert.equal(bluezQueries, 2);
|
|
|
|
context.root.queryBluezCodecState = (_device, callback) => callback([], "");
|
|
context.refreshDeviceCodec(device);
|
|
assert.deepEqual(updates, ["LHDC v5", "Unknown"]);
|
|
context.getAvailableCodecs(device, (codecs, current, currentIndex) => {
|
|
assert.equal(codecs, pipewire);
|
|
assert.equal(current, "Unknown");
|
|
assert.equal(currentIndex, 3);
|
|
});
|
|
context.root.queryBluezCodecState = (_device, callback) => callback([], "VENDOR");
|
|
context.refreshDeviceCodec(device);
|
|
assert.equal(updates.at(-1), "Unknown");
|
|
context.getAvailableCodecs(device, (codecs, current) => {
|
|
assert.equal(codecs, pipewire);
|
|
assert.equal(current, "Unknown");
|
|
});
|
|
context.root.dbusBridgeAvailable = false;
|
|
context.getAvailableCodecs(device, (codecs, current) => {
|
|
assert.equal(codecs, pipewire);
|
|
assert.equal(current, "Unknown");
|
|
});
|
|
});
|
|
|
|
test("a late BlueZ lookup cannot undo a successful codec switch", () => {
|
|
const context = methods(["refreshDeviceCodec", "invalidateCodecQuery", "updateDeviceCodec", "codecQueryIsCurrent"]);
|
|
const device = { connected: true, address: "AA:BB" };
|
|
const updates = [];
|
|
let reply;
|
|
context.isAudioDevice = () => true;
|
|
context.whenCodecBackendReady = callback => callback();
|
|
context.root = context;
|
|
context.wpexecAvailable = true;
|
|
context.dbusBridgeAvailable = true;
|
|
context.adapter = { devices: { values: [device] } };
|
|
context.codecQueryVersions = {};
|
|
context.deviceCodecs = {};
|
|
context.queryCardProfiles = (_device, callback) => callback([], "", 3);
|
|
context.queryBluezCodecState = (_device, callback) => { reply = callback; };
|
|
context.updateDeviceCodec = (_address, name) => updates.push(name);
|
|
|
|
context.refreshDeviceCodec(device);
|
|
assert.equal(typeof reply, "function");
|
|
// The selector invalidates its pending lookup when switching starts.
|
|
context.invalidateCodecQuery(device.address);
|
|
context.updateDeviceCodec(device.address, "AAC");
|
|
reply([], "LDAC");
|
|
assert.deepEqual(updates, ["AAC"]);
|
|
});
|
|
|
|
test("only the newest refresh for a device can publish a codec", () => {
|
|
const context = methods(["refreshDeviceCodec", "invalidateCodecQuery", "codecQueryIsCurrent"]);
|
|
const device = { connected: true, address: "AA:BB" };
|
|
const other = { connected: true, address: "CC:DD" };
|
|
const replies = new Map();
|
|
const updates = [];
|
|
context.isAudioDevice = () => true;
|
|
context.whenCodecBackendReady = callback => callback();
|
|
context.root = context;
|
|
context.wpexecAvailable = true;
|
|
context.dbusBridgeAvailable = true;
|
|
context.adapter = { devices: { values: [device, other] } };
|
|
context.codecQueryVersions = {};
|
|
context.queryCardProfiles = (_device, callback) => callback([], "", 3);
|
|
context.queryBluezCodecState = (target, callback) => {
|
|
const callbacks = replies.get(target.address) || [];
|
|
callbacks.push(callback);
|
|
replies.set(target.address, callbacks);
|
|
};
|
|
context.updateDeviceCodec = (address, name) => updates.push([address, name]);
|
|
|
|
context.refreshDeviceCodec(device);
|
|
context.refreshDeviceCodec(other);
|
|
context.refreshDeviceCodec(device);
|
|
replies.get(device.address)[0]([], "LDAC");
|
|
replies.get(other.address)[0]([], "SBC");
|
|
replies.get(device.address)[1]([], "AAC");
|
|
assert.deepEqual(updates, [["CC:DD", "SBC"], ["AA:BB", "AAC"]]);
|
|
});
|
|
|
|
test("a replaced adapter cannot receive a late BlueZ codec", () => {
|
|
const context = methods(["refreshDeviceCodec", "invalidateCodecQuery", "codecQueryIsCurrent"]);
|
|
const device = { connected: true, address: "AA:BB" };
|
|
const updates = [];
|
|
let reply;
|
|
context.isAudioDevice = () => true;
|
|
context.whenCodecBackendReady = callback => callback();
|
|
context.root = context;
|
|
context.wpexecAvailable = true;
|
|
context.dbusBridgeAvailable = true;
|
|
context.adapter = { dbusPath: "/adapter/old", devices: { values: [device] } };
|
|
context.codecQueryVersions = {};
|
|
context.queryCardProfiles = (_device, callback) => callback([], "", 3);
|
|
context.queryBluezCodecState = (_device, callback) => { reply = callback; };
|
|
context.updateDeviceCodec = (_address, name) => updates.push(name);
|
|
|
|
context.refreshDeviceCodec(device);
|
|
context.adapter = { dbusPath: "/adapter/new", devices: { values: [device] } };
|
|
reply([], "LDAC");
|
|
assert.deepEqual(updates, []);
|
|
});
|
|
|
|
test("a disconnected device cannot receive a late BlueZ codec", () => {
|
|
const context = methods(["refreshDeviceCodec", "invalidateCodecQuery", "codecQueryIsCurrent"]);
|
|
const device = { connected: true, address: "AA:BB" };
|
|
let reply;
|
|
const updates = [];
|
|
context.isAudioDevice = () => true;
|
|
context.whenCodecBackendReady = callback => callback();
|
|
context.root = context;
|
|
context.wpexecAvailable = true;
|
|
context.dbusBridgeAvailable = true;
|
|
context.adapter = { devices: { values: [device] } };
|
|
context.codecQueryVersions = {};
|
|
context.queryCardProfiles = (_device, callback) => callback([], "", 3);
|
|
context.queryBluezCodecState = (_device, callback) => { reply = callback; };
|
|
context.updateDeviceCodec = (_address, name) => updates.push(name);
|
|
|
|
context.refreshDeviceCodec(device);
|
|
device.connected = false;
|
|
reply([], "LDAC");
|
|
assert.deepEqual(updates, []);
|
|
});
|
|
|
|
test("known PipeWire codec does not query BlueZ again", () => {
|
|
const context = methods(["refreshDeviceCodec", "getAvailableCodecs", "invalidateCodecQuery", "codecQueryIsCurrent"]);
|
|
const device = { connected: true, address: "AA:BB" };
|
|
const pipewire = [{ name: "LHDC v5", index: 3 }];
|
|
const updates = [];
|
|
context.isAudioDevice = () => true;
|
|
context.whenCodecBackendReady = callback => callback();
|
|
context.root = {
|
|
invalidateCodecQuery: context.invalidateCodecQuery,
|
|
codecQueryIsCurrent: context.codecQueryIsCurrent,
|
|
codecQueryVersions: {},
|
|
adapter: { devices: { values: [device] } },
|
|
wpexecAvailable: true, dbusBridgeAvailable: true,
|
|
queryCardProfiles: (_device, callback) => callback(pipewire, "LHDC v5", 3),
|
|
queryBluezCodecState: () => assert.fail("known current codec should not query BlueZ"),
|
|
updateDeviceCodec: (_address, name) => updates.push(name)
|
|
};
|
|
context.refreshDeviceCodec(device);
|
|
assert.deepEqual(updates, ["LHDC v5"]);
|
|
context.getAvailableCodecs(device, (codecs, current) => {
|
|
assert.equal(codecs, pipewire);
|
|
assert.equal(current, "LHDC v5");
|
|
});
|
|
});
|
|
|
|
test("vendor codecs are read from the BlueZ configuration bytes", () => {
|
|
const context = methods(["bytesFromDbusValue", "codecNameFromBluez"]);
|
|
const nameOf = (vendor, codec) => context.codecNameFromBluez(0xff, bluezConfiguration(vendor, codec));
|
|
assert.equal(nameOf(0x0000004f, 0x0001), "APTX");
|
|
assert.equal(nameOf(0x000000d7, 0x0024), "APTX_HD");
|
|
assert.equal(nameOf(0x0000012d, 0x00aa), "LDAC");
|
|
assert.equal(nameOf(0x0000053a, 0x4c35), "LHDC_V5");
|
|
assert.equal(nameOf(0x0000053a, 0x4c33), "LHDC_V3");
|
|
assert.equal(nameOf(0x0000ffff, 0x1234), "VENDOR");
|
|
assert.equal(context.codecNameFromBluez(0x00, []), "SBC");
|
|
assert.equal(context.codecNameFromBluez(0x02, []), "AAC");
|
|
assert.equal(context.codecNameFromBluez(0x04, []), "ATRAC");
|
|
assert.equal(context.codecNameFromBluez(0x03, []), "");
|
|
assert.equal(context.codecNameFromBluez(0xff, [0x3a, 0x05]), "VENDOR");
|
|
assert.equal(context.codecNameFromBluez(0xff, []), "VENDOR");
|
|
});
|
|
|
|
test("LHDC codecs carry a name, description and category", () => {
|
|
const context = methods(["codecMap", "getCodecInfo", "codecCategory", "codecNameFromProfile"]);
|
|
const v5 = context.getCodecInfo("LHDC_V5");
|
|
assert.equal(v5.name, "LHDC v5");
|
|
assert.notEqual(v5.description, "Unknown codec");
|
|
assert.equal(context.getCodecInfo("lhdc-v5").name, "LHDC v5");
|
|
assert.equal(context.getCodecInfo("LHDC_V3").name, "LHDC v3");
|
|
assert.equal(context.codecCategory(v5.name, "a2dp-sink"), "media");
|
|
assert.equal(context.codecCategory(v5.name, "headset-head-unit"), "call");
|
|
assert.equal(context.codecNameFromProfile("a2dp-sink"), "");
|
|
assert.equal(context.codecNameFromProfile("a2dp-sink-aac"), "AAC");
|
|
});
|
|
|
|
// BlueZ Alias falls back to the dashed address when a device sends no name.
|
|
test("scan list sorts named devices above alias-is-address devices", () => {
|
|
const context = methods(["hasRealName", "sortDevices"]);
|
|
const sorted = context.sortDevices([
|
|
{ name: "23-5F-24-05-F3-4A", address: "23:5F:24:05:F3:4A" },
|
|
{ name: "ATH-M20xBT", address: "AA:BB:CC:DD:EE:FF" },
|
|
{ name: "", deviceName: "", address: "01:02:03:04:05:06" }
|
|
]);
|
|
assert.deepEqual(sorted.map(d => d.address), ["AA:BB:CC:DD:EE:FF", "01:02:03:04:05:06", "23:5F:24:05:F3:4A"]);
|
|
});
|