@7365admin1/core 3.84.0 → 3.86.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/CHANGELOG.md +44 -0
- package/dist/index.d.ts +236 -86
- package/dist/index.js +674 -173
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +662 -170
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/demo-adapter.test.mjs +164 -0
- package/test/robot-adapter.test.mjs +83 -32
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7365admin1/core",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.86.0",
|
|
5
5
|
"author": "7365admin1",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"build": "tsup src/index.ts --format cjs,esm --dts && shx cp -r src/public dist && shx cp -r src/utils/handlebars dist",
|
|
14
14
|
"release": "yarn run build && changeset publish",
|
|
15
15
|
"lint": "tsc",
|
|
16
|
-
"test": "tsup src/config.ts src/utils/building-map.util.ts src/utils/robot-cloud.util.ts src/utils/robot-adapter.util.ts src/utils/robot-tracking.util.ts src/utils/robot-apps.util.ts src/utils/robot-permission.util.ts src/utils/lift-contract.util.ts src/utils/anpr-revoke.util.ts src/utils/anpr-plate.util.ts src/utils/vehicle-request.util.ts src/utils/camera-view.util.ts src/utils/camera-bulk-import.util.ts src/utils/dashboard-metrics.util.ts src/utils/camera-capability.util.ts src/utils/camera-playback.util.ts src/services/camera-device-http.service.ts src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts src/utils/site-name.util.ts src/utils/service-provider-invite.util.ts src/utils/notification-category.util.ts src/utils/hid-card.util.ts src/utils/hid-json.util.ts src/utils/hid-access.util.ts src/models/customer-site.model.ts src/models/site-camera.model.ts src/models/member.model.ts src/models/subscription.model.ts src/utils/session-id.util.ts src/utils/occurrence-query.util.ts src/utils/occurrence-book-create.util.ts src/utils/update-result.util.ts src/utils/hid-amico-authz.util.ts src/utils/promo-code-currency.util.ts src/utils/console-audit.util.ts src/utils/org-suspension.util.ts src/utils/session-revoke.util.ts src/models/hid-amico.model.ts src/models/patrol-email.model.ts src/utils/patrol-email-pdf.util.ts src/utils/visitor-status.util.ts src/utils/self-service-email.util.ts src/utils/hid-secret.util.ts src/models/visitor-invite.model.ts src/models/guard-invite.model.ts src/utils/site-timezone.util.ts src/utils/bulletin-service-apps.util.ts src/models/bulletin-board.model.ts src/utils/vehicle-import.util.ts src/utils/expired-vehicle-sweep.util.ts src/models/vehicle.model.ts src/controllers/chat.controller.ts src/utils/file-owner.util.ts src/utils/console-permission.util.ts src/utils/role-scope.util.ts src/utils/org-default-roles.util.ts src/utils/org-modules.util.ts src/utils/site-modules.util.ts src/utils/resident-app-modules.util.ts src/utils/resident-default-role.util.ts src/utils/invite-role-scope.util.ts src/models/personal-emergency-chain.model.ts src/utils/self-signup-files.util.ts src/models/person.model.ts src/utils/module-gate.util.ts src/utils/member-lookup.util.ts src/utils/notification-access.util.ts src/utils/module-access.util.ts src/services/module-gate.service.ts src/controllers/site.controller.ts src/controllers/organization.controller.ts src/controllers/member.controller.ts src/utils/org-owner-repair.util.ts src/utils/socket-session.util.ts src/utils/people-create-gate.util.ts src/utils/service-app-type.util.ts --format esm --out-dir test/.build --no-dts --silent && node --test test/*.test.mjs",
|
|
16
|
+
"test": "tsup src/config.ts src/utils/building-map.util.ts src/utils/robot-cloud.util.ts src/utils/robot-adapter.util.ts src/utils/png-encoder.util.ts src/services/demo.adapter.ts src/utils/robot-tracking.util.ts src/utils/robot-apps.util.ts src/utils/robot-permission.util.ts src/utils/lift-contract.util.ts src/utils/anpr-revoke.util.ts src/utils/anpr-plate.util.ts src/utils/vehicle-request.util.ts src/utils/camera-view.util.ts src/utils/camera-bulk-import.util.ts src/utils/dashboard-metrics.util.ts src/utils/camera-capability.util.ts src/utils/camera-playback.util.ts src/services/camera-device-http.service.ts src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts src/utils/site-name.util.ts src/utils/service-provider-invite.util.ts src/utils/notification-category.util.ts src/utils/hid-card.util.ts src/utils/hid-json.util.ts src/utils/hid-access.util.ts src/models/customer-site.model.ts src/models/site-camera.model.ts src/models/member.model.ts src/models/subscription.model.ts src/utils/session-id.util.ts src/utils/occurrence-query.util.ts src/utils/occurrence-book-create.util.ts src/utils/update-result.util.ts src/utils/hid-amico-authz.util.ts src/utils/promo-code-currency.util.ts src/utils/console-audit.util.ts src/utils/org-suspension.util.ts src/utils/session-revoke.util.ts src/models/hid-amico.model.ts src/models/patrol-email.model.ts src/utils/patrol-email-pdf.util.ts src/utils/visitor-status.util.ts src/utils/self-service-email.util.ts src/utils/hid-secret.util.ts src/models/visitor-invite.model.ts src/models/guard-invite.model.ts src/utils/site-timezone.util.ts src/utils/bulletin-service-apps.util.ts src/models/bulletin-board.model.ts src/utils/vehicle-import.util.ts src/utils/expired-vehicle-sweep.util.ts src/models/vehicle.model.ts src/controllers/chat.controller.ts src/utils/file-owner.util.ts src/utils/console-permission.util.ts src/utils/role-scope.util.ts src/utils/org-default-roles.util.ts src/utils/org-modules.util.ts src/utils/site-modules.util.ts src/utils/resident-app-modules.util.ts src/utils/resident-default-role.util.ts src/utils/invite-role-scope.util.ts src/models/personal-emergency-chain.model.ts src/utils/self-signup-files.util.ts src/models/person.model.ts src/utils/module-gate.util.ts src/utils/member-lookup.util.ts src/utils/notification-access.util.ts src/utils/module-access.util.ts src/services/module-gate.service.ts src/controllers/site.controller.ts src/controllers/organization.controller.ts src/controllers/member.controller.ts src/utils/org-owner-repair.util.ts src/utils/socket-session.util.ts src/utils/people-create-gate.util.ts src/utils/service-app-type.util.ts --format esm --out-dir test/.build --no-dts --silent && node --test test/*.test.mjs",
|
|
17
17
|
"test:camera": "yarn test",
|
|
18
18
|
"test:e2e": "yarn build && node --test --test-concurrency=1 --test-timeout=600000 \"test/e2e/*.test.mjs\""
|
|
19
19
|
},
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { createDemoAdapter, DEMO_CAPABILITIES } from "./.build/services/demo.adapter.mjs";
|
|
5
|
+
import { robotTier, commandSupported } from "./.build/utils/robot-adapter.util.mjs";
|
|
6
|
+
import { encodeGreyscalePng } from "./.build/utils/png-encoder.util.mjs";
|
|
7
|
+
import { wallsFromOccupancyGrid } from "./.build/utils/building-map.util.mjs";
|
|
8
|
+
|
|
9
|
+
/* --------------------------------------------------------------------------
|
|
10
|
+
* The demo brand has two jobs, and these guard both.
|
|
11
|
+
*
|
|
12
|
+
* It makes the work visible: until a vendor supplies credentials, every robot
|
|
13
|
+
* screen in the platform is empty and no button can be pressed.
|
|
14
|
+
*
|
|
15
|
+
* And it proves the vendor-neutral design. It is a second brand written against
|
|
16
|
+
* the same contract as Ounitech, declaring DIFFERENT capabilities. If the
|
|
17
|
+
* screens behave differently for the two without being told which is which,
|
|
18
|
+
* the platform really is brand-agnostic.
|
|
19
|
+
* ----------------------------------------------------------------------- */
|
|
20
|
+
|
|
21
|
+
const at = (ms) => createDemoAdapter({ now: () => ms });
|
|
22
|
+
|
|
23
|
+
test("the demo brand reaches Full Control, where Ounitech reaches none", () => {
|
|
24
|
+
// The contrast IS the demonstration: same contract, different capabilities,
|
|
25
|
+
// and the screens draw different controls without being told to.
|
|
26
|
+
assert.equal(robotTier(DEMO_CAPABILITIES), "full-control");
|
|
27
|
+
|
|
28
|
+
const ounitech = { readLocation: true, sendToDock: true, startProcess: true };
|
|
29
|
+
assert.equal(robotTier(ounitech), "no-interface");
|
|
30
|
+
|
|
31
|
+
// So the demo offers pause; Ounitech does not.
|
|
32
|
+
assert.equal(commandSupported(DEMO_CAPABILITIES, "pause"), true);
|
|
33
|
+
assert.equal(commandSupported(ounitech, "pause"), false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("it lists robots across more than one floor", () => {
|
|
37
|
+
return at(0).listRobots().then((robots) => {
|
|
38
|
+
assert.ok(robots.length >= 3);
|
|
39
|
+
assert.ok(new Set(robots.map((r) => r.floorNo)).size >= 2, "more than one floor");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("every demo robot says it is a demo, in its own name", async () => {
|
|
44
|
+
// A simulated robot appearing in a real estate's list without saying so would
|
|
45
|
+
// be worse than no demo at all.
|
|
46
|
+
const robots = await at(0).listRobots();
|
|
47
|
+
for (const r of robots) {
|
|
48
|
+
assert.match(r.name, /demo/i, r.name);
|
|
49
|
+
assert.match(r.robotSn, /^DEMO-/, r.robotSn);
|
|
50
|
+
}
|
|
51
|
+
assert.match(at(0).displayName, /demo/i);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("it reports a KNOWN floor and a real position — unlike Ounitech", async () => {
|
|
55
|
+
// Which is what lets the 3D layout actually draw the robot.
|
|
56
|
+
const [robot] = await at(0).listRobots();
|
|
57
|
+
assert.equal(robot.floorKnown, true);
|
|
58
|
+
assert.equal(typeof robot.floorNo, "number");
|
|
59
|
+
assert.ok(robot.position);
|
|
60
|
+
assert.equal(typeof robot.position.x, "number");
|
|
61
|
+
assert.equal(typeof robot.position.theta, "number");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("the robots move as the clock advances", async () => {
|
|
65
|
+
const [early] = await at(0).listRobots();
|
|
66
|
+
const [later] = await at(30_000).listRobots();
|
|
67
|
+
assert.notDeepEqual(early.position, later.position, "a demo robot that never moves is a picture");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("battery changes over time and stays in range", async () => {
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
for (const ms of [0, 600_000, 1_800_000, 3_000_000]) {
|
|
73
|
+
const [robot] = await at(ms).listRobots();
|
|
74
|
+
assert.ok(robot.batteryPercent >= 0 && robot.batteryPercent <= 100, String(robot.batteryPercent));
|
|
75
|
+
seen.add(robot.batteryPercent);
|
|
76
|
+
}
|
|
77
|
+
assert.ok(seen.size > 1, "the battery never changed");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("a command changes what the robot reports, and then wears off", async () => {
|
|
81
|
+
const adapter = at(1_000_000);
|
|
82
|
+
const before = await adapter.getRobot("DEMO-0001");
|
|
83
|
+
|
|
84
|
+
await adapter.sendCommand("DEMO-0001", { kind: "full" });
|
|
85
|
+
const during = await at(1_000_000).getRobot("DEMO-0001");
|
|
86
|
+
assert.equal(during.mode, "WORKING", `was ${before.mode}`);
|
|
87
|
+
|
|
88
|
+
// It returns to idle by itself, so the demo needs no resetting between
|
|
89
|
+
// viewings.
|
|
90
|
+
const after = await at(1_000_000 + 11 * 60_000).getRobot("DEMO-0001");
|
|
91
|
+
assert.notEqual(after.mode, "WORKING");
|
|
92
|
+
|
|
93
|
+
await adapter.sendCommand("DEMO-0001", { kind: "cancel" });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("commanding a robot that does not exist is refused by name", async () => {
|
|
97
|
+
await assert.rejects(
|
|
98
|
+
() => at(0).sendCommand("NOT-A-ROBOT", { kind: "full" }),
|
|
99
|
+
/No demo robot with serial NOT-A-ROBOT/,
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("an unknown serial returns null rather than a stranger's robot", async () => {
|
|
104
|
+
assert.equal(await at(0).getRobot("DEMO-9999"), null);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/* --------------------------------------------------------- the floor plans */
|
|
108
|
+
|
|
109
|
+
test("the plans carry the scale and areas the map screens need", async () => {
|
|
110
|
+
const maps = await at(0).getMaps();
|
|
111
|
+
assert.ok(maps.length >= 2, "more than one level");
|
|
112
|
+
for (const m of maps) {
|
|
113
|
+
// 1px = 5cm, traffic_editor's default and Ounitech's own resolution.
|
|
114
|
+
assert.equal(m.resolution, 0.05);
|
|
115
|
+
assert.ok(m.areaList.length >= 1);
|
|
116
|
+
assert.ok(m.backgroundFileKey);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("the generated plan is a real PNG a browser will accept", async () => {
|
|
121
|
+
const files = await at(0).getMapImages("DEMO-0001", ["demo/L1.png"]);
|
|
122
|
+
const url = files[0].url;
|
|
123
|
+
assert.match(url, /^data:image\/png;base64,/);
|
|
124
|
+
|
|
125
|
+
// The 8-byte PNG signature, so this is decodable rather than merely base64.
|
|
126
|
+
const bytes = Buffer.from(url.split(",")[1], "base64");
|
|
127
|
+
assert.deepEqual([...bytes.subarray(0, 8)], [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
128
|
+
assert.equal(bytes.subarray(12, 16).toString("ascii"), "IHDR");
|
|
129
|
+
assert.equal(bytes.subarray(bytes.length - 8, bytes.length - 4).toString("ascii"), "IEND");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("a data URI is used so the canvas is not tainted", async () => {
|
|
133
|
+
// The layout is built by reading these pixels BACK. A cross-origin image
|
|
134
|
+
// would throw on getImageData, which looks like a broken floor plan.
|
|
135
|
+
const files = await at(0).getMapImages("DEMO-0001", ["demo/L1.png", "demo/L2.png"]);
|
|
136
|
+
assert.equal(files.length, 2);
|
|
137
|
+
for (const f of files) assert.ok(f.url.startsWith("data:"));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("the two levels are genuinely different plans", async () => {
|
|
141
|
+
const [l1, l2] = await at(0).getMapImages("DEMO-0001", ["demo/L1.png", "demo/L2.png"]);
|
|
142
|
+
assert.notEqual(l1.url, l2.url, "both floors rendered the same picture");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("THE POINT: the generated plan yields a 3D layout", () => {
|
|
146
|
+
// End to end, with no vendor and no network: a plan becomes walls.
|
|
147
|
+
const width = 60, height = 40;
|
|
148
|
+
const px = new Uint8Array(width * height).fill(255);
|
|
149
|
+
for (let x = 0; x < width; x++) { px[x] = 0; px[(height - 1) * width + x] = 0; }
|
|
150
|
+
for (let y = 0; y < height; y++) { px[y * width] = 0; px[y * width + width - 1] = 0; }
|
|
151
|
+
|
|
152
|
+
const png = encodeGreyscalePng(px, width, height);
|
|
153
|
+
assert.ok(png.length > 0);
|
|
154
|
+
|
|
155
|
+
const { walls } = wallsFromOccupancyGrid({ data: px, width, height }, { minRunPx: 5 });
|
|
156
|
+
assert.equal(walls.length, 4, "a closed room is four walls");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
/* -------------------------------------------------------- the PNG encoder */
|
|
160
|
+
|
|
161
|
+
test("the encoder refuses an impossible image rather than emitting a broken one", () => {
|
|
162
|
+
assert.throws(() => encodeGreyscalePng(new Uint8Array(4), 0, 4), /positive size/);
|
|
163
|
+
assert.throws(() => encodeGreyscalePng(new Uint8Array(2), 10, 10), /one byte per pixel/);
|
|
164
|
+
});
|
|
@@ -3,12 +3,13 @@ import { test } from "node:test";
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
ROBOT_CAPABILITIES,
|
|
6
|
-
|
|
6
|
+
adapterForRobot,
|
|
7
|
+
clearRobotBrands,
|
|
7
8
|
commandSupported,
|
|
8
9
|
missingForTier,
|
|
9
|
-
|
|
10
|
+
registerRobotBrand,
|
|
10
11
|
registeredRobotBrands,
|
|
11
|
-
|
|
12
|
+
robotBrandFor,
|
|
12
13
|
robotTier,
|
|
13
14
|
supports,
|
|
14
15
|
uncontrollableFleetWarning,
|
|
@@ -136,44 +137,94 @@ test("an unknown command kind is refused, not defaulted", () => {
|
|
|
136
137
|
|
|
137
138
|
/* --------------------------------------------------------------- registry */
|
|
138
139
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
const brand = (over = {}) => ({
|
|
141
|
+
brand: "brand-a",
|
|
142
|
+
displayName: "Brand A",
|
|
143
|
+
capabilities: ALL,
|
|
144
|
+
create: () => ({ brand: "brand-a", displayName: "Brand A", capabilities: ALL,
|
|
145
|
+
listRobots: async () => [], getRobot: async () => null }),
|
|
146
|
+
...over,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const robot = (over = {}) => ({
|
|
150
|
+
name: "Cleaner 02", brand: "brand-a", appKey: "k", appToken: "s", ...over,
|
|
151
|
+
});
|
|
143
152
|
|
|
144
|
-
|
|
145
|
-
|
|
153
|
+
test("a brand is found by name, and never by fallback", () => {
|
|
154
|
+
clearRobotBrands();
|
|
155
|
+
registerRobotBrand(brand());
|
|
156
|
+
|
|
157
|
+
assert.equal(robotBrandFor("brand-a")?.brand, "brand-a");
|
|
158
|
+
// Falling back to the only registered brand would send one brand's robot a
|
|
146
159
|
// command in a protocol it does not speak.
|
|
147
|
-
assert.equal(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
assert.equal(robotAdapterFor(42), null);
|
|
160
|
+
assert.equal(robotBrandFor("brand-b"), null);
|
|
161
|
+
for (const bad of ["", null, undefined, 42]) {
|
|
162
|
+
assert.equal(robotBrandFor(bad), null, String(bad));
|
|
163
|
+
}
|
|
152
164
|
});
|
|
153
165
|
|
|
154
|
-
test("
|
|
155
|
-
|
|
156
|
-
assert.throws(() =>
|
|
157
|
-
assert.throws(() =>
|
|
166
|
+
test("a brand without a name or a factory is refused at registration", () => {
|
|
167
|
+
clearRobotBrands();
|
|
168
|
+
assert.throws(() => registerRobotBrand({ displayName: "x" }), /must name itself/);
|
|
169
|
+
assert.throws(() => registerRobotBrand(null), /must name itself/);
|
|
170
|
+
// A brand registered with no way to build an adapter would fail later, at the
|
|
171
|
+
// moment somebody tried to use a robot - not here, where it is fixable.
|
|
172
|
+
assert.throws(
|
|
173
|
+
() => registerRobotBrand({ brand: "x", displayName: "X", capabilities: {} }),
|
|
174
|
+
/no way to build an adapter/,
|
|
175
|
+
);
|
|
158
176
|
});
|
|
159
177
|
|
|
160
|
-
test("
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
178
|
+
test("an adapter is built per robot, from THAT robot's credentials", () => {
|
|
179
|
+
clearRobotBrands();
|
|
180
|
+
let seen = null;
|
|
181
|
+
registerRobotBrand(brand({ create: (c) => { seen = c; return { brand: "brand-a",
|
|
182
|
+
displayName: "Brand A", capabilities: ALL, listRobots: async () => [],
|
|
183
|
+
getRobot: async () => null }; } }));
|
|
164
184
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
185
|
+
adapterForRobot(robot({ appKey: "estate-1-id", appToken: "estate-1-secret" }));
|
|
186
|
+
assert.deepEqual(seen, { appid: "estate-1-id", secret: "estate-1-secret" });
|
|
187
|
+
|
|
188
|
+
// One estate's credentials must never sign another estate's request, which is
|
|
189
|
+
// why the adapter is constructed per robot rather than per brand.
|
|
190
|
+
adapterForRobot(robot({ appKey: "estate-2-id", appToken: "estate-2-secret" }));
|
|
191
|
+
assert.deepEqual(seen, { appid: "estate-2-id", secret: "estate-2-secret" });
|
|
169
192
|
});
|
|
170
193
|
|
|
171
|
-
test("
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
194
|
+
test("an unknown brand is refused BY NAME, not with a null dereference", () => {
|
|
195
|
+
clearRobotBrands();
|
|
196
|
+
registerRobotBrand(brand());
|
|
197
|
+
assert.throws(() => adapterForRobot(robot({ brand: "unregistered" })),
|
|
198
|
+
/No robot adapter is registered for the brand "unregistered"/);
|
|
199
|
+
assert.throws(() => adapterForRobot(robot({ brand: "" })),
|
|
200
|
+
/does not say which brand it is/);
|
|
201
|
+
});
|
|
176
202
|
|
|
203
|
+
test("a robot with no credentials is refused before any call is attempted", () => {
|
|
204
|
+
clearRobotBrands();
|
|
205
|
+
registerRobotBrand(brand());
|
|
206
|
+
assert.throws(() => adapterForRobot(robot({ appKey: "" })), /no cloud credentials/);
|
|
207
|
+
assert.throws(() => adapterForRobot(robot({ appToken: " " })), /no cloud credentials/);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("the brand list reports each brand's tier, for a settings picker", () => {
|
|
211
|
+
clearRobotBrands();
|
|
212
|
+
registerRobotBrand(brand({ brand: "good", displayName: "Good", capabilities: ALL }));
|
|
213
|
+
registerRobotBrand(brand({ brand: "poor", displayName: "Poor", capabilities: caps("readLocation") }));
|
|
214
|
+
|
|
215
|
+
assert.deepEqual(
|
|
216
|
+
registeredRobotBrands().sort((x, y) => x.brand.localeCompare(y.brand)),
|
|
217
|
+
[
|
|
218
|
+
{ brand: "good", displayName: "Good", tier: "full-control" },
|
|
219
|
+
{ brand: "poor", displayName: "Poor", tier: "no-interface" },
|
|
220
|
+
],
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("re-registering a brand replaces it rather than duplicating", () => {
|
|
225
|
+
clearRobotBrands();
|
|
226
|
+
registerRobotBrand(brand({ displayName: "One" }));
|
|
227
|
+
registerRobotBrand(brand({ displayName: "Two" }));
|
|
177
228
|
assert.equal(registeredRobotBrands().length, 1);
|
|
178
|
-
assert.equal(
|
|
229
|
+
assert.equal(robotBrandFor("brand-a").displayName, "Two");
|
|
179
230
|
});
|