@robotical/martyblocksjr 4.2.3 → 4.2.5
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.
|
@@ -59,6 +59,15 @@
|
|
|
59
59
|
padding-top: 2px;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
.connectButton:focus-visible {
|
|
63
|
+
outline: ${css_vh(0.3)} solid #133C46;
|
|
64
|
+
outline-offset: ${css_vh(0.15)};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.connectButton.connectButtonPointerFocus:focus {
|
|
68
|
+
outline: none;
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
.extensionAddButton {
|
|
63
72
|
position: absolute;
|
|
64
73
|
right: -${css_vh(1.45)};
|
|
@@ -68,7 +77,7 @@
|
|
|
68
77
|
height: ${css_vh(5)};
|
|
69
78
|
background: transparent;
|
|
70
79
|
border: 0;
|
|
71
|
-
display:
|
|
80
|
+
display: none !important;
|
|
72
81
|
justify-content: center;
|
|
73
82
|
align-items: center;
|
|
74
83
|
cursor: pointer;
|
|
@@ -107,8 +116,11 @@
|
|
|
107
116
|
.connectButtonConnected {
|
|
108
117
|
/* background: #4bfaa3; */
|
|
109
118
|
display: grid;
|
|
110
|
-
grid-template-rows:
|
|
111
|
-
|
|
119
|
+
grid-template-rows:
|
|
120
|
+
minmax(0, 2.2fr)
|
|
121
|
+
minmax(0, 1fr)
|
|
122
|
+
minmax(0, 0.95fr);
|
|
123
|
+
grid-template-columns: minmax(0, 1fr);
|
|
112
124
|
justify-content: space-between;
|
|
113
125
|
align-items: center;
|
|
114
126
|
flex-direction: column;
|
|
@@ -116,14 +128,22 @@
|
|
|
116
128
|
}
|
|
117
129
|
|
|
118
130
|
.connectButtonConnected .connectIcon {
|
|
119
|
-
margin-bottom:
|
|
131
|
+
margin-bottom: 0;
|
|
120
132
|
display: grid;
|
|
121
|
-
width: ${css_vh(4.5)};
|
|
133
|
+
width: ${css_vh(4.5)};
|
|
134
|
+
height: 100%;
|
|
122
135
|
align-items: center;
|
|
123
136
|
align-self: center;
|
|
124
137
|
justify-self: center;
|
|
125
138
|
}
|
|
126
139
|
|
|
140
|
+
.connectButtonConnected .connectIcon svg,
|
|
141
|
+
.connectButtonConnected .batteryAndSignalContainer svg {
|
|
142
|
+
display: block;
|
|
143
|
+
width: 100%;
|
|
144
|
+
height: 100%;
|
|
145
|
+
}
|
|
146
|
+
|
|
127
147
|
.iconButtonContainer {
|
|
128
148
|
width: 65%;
|
|
129
149
|
height: 20%;
|
|
@@ -142,32 +162,46 @@
|
|
|
142
162
|
} */
|
|
143
163
|
.connectedButtonContainer {
|
|
144
164
|
position: absolute;
|
|
145
|
-
top:
|
|
146
|
-
left:
|
|
147
|
-
width:
|
|
148
|
-
height:
|
|
165
|
+
top: -${css_vh(0.9)};
|
|
166
|
+
left: -${css_vh(0.9)};
|
|
167
|
+
width: ${css_vh(3)};
|
|
168
|
+
height: ${css_vh(3)};
|
|
149
169
|
background-image: url('../assets/connection/disconnect_btn-default.svg');
|
|
150
170
|
}
|
|
151
|
-
|
|
152
|
-
|
|
171
|
+
@media (hover: hover) and (pointer: fine) {
|
|
172
|
+
.connectButton:hover .connectedButtonContainer {
|
|
173
|
+
background-image: url('../assets/connection/disconnect_btn-hover.svg');
|
|
174
|
+
}
|
|
153
175
|
}
|
|
154
176
|
|
|
155
177
|
.raftNameConnectButton {
|
|
156
178
|
display: flex;
|
|
179
|
+
box-sizing: border-box;
|
|
180
|
+
min-width: 0;
|
|
181
|
+
max-width: 100%;
|
|
157
182
|
align-items: center;
|
|
158
|
-
justify-content:
|
|
183
|
+
justify-content: flex-start;
|
|
159
184
|
color: #17351d;
|
|
160
185
|
align-self: center;
|
|
161
186
|
justify-self: center;
|
|
162
187
|
background: #89c761;
|
|
163
188
|
font-family: 'Lato Regular';
|
|
164
|
-
|
|
189
|
+
font-size: ${css_vh(1.25)};
|
|
190
|
+
line-height: 1;
|
|
191
|
+
padding: 0 ${css_vh(0.15)};
|
|
192
|
+
text-align: left;
|
|
193
|
+
white-space: nowrap;
|
|
194
|
+
overflow: hidden;
|
|
195
|
+
text-overflow: ellipsis;
|
|
165
196
|
height: 100%;
|
|
166
197
|
width: 100%;
|
|
167
198
|
border-radius: 4px;
|
|
168
199
|
}
|
|
169
200
|
|
|
170
201
|
.batteryAndSignalContainer {
|
|
202
|
+
box-sizing: border-box;
|
|
203
|
+
min-width: 0;
|
|
204
|
+
max-width: 100%;
|
|
171
205
|
width: 100%;
|
|
172
206
|
display: flex;
|
|
173
207
|
/* justify-content: space-evenly; */
|
|
@@ -177,7 +211,14 @@
|
|
|
177
211
|
}
|
|
178
212
|
|
|
179
213
|
.connectButtonConnected .batteryAndSignalContainer {
|
|
180
|
-
|
|
214
|
+
height: 100%;
|
|
215
|
+
margin-bottom: 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.connectButtonConnected .batteryIndicatorContainer,
|
|
219
|
+
.connectButtonConnected .signalIndicatorContainer {
|
|
220
|
+
height: 100%;
|
|
221
|
+
align-items: center;
|
|
181
222
|
}
|
|
182
223
|
|
|
183
224
|
.batteryIndicatorContainer {
|
package/package.json
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
+
const { newHTMLMock } = vi.hoisted(() => ({
|
|
4
|
+
newHTMLMock: vi.fn()
|
|
5
|
+
}));
|
|
6
|
+
|
|
3
7
|
vi.mock('@/editor/ScratchJr', () => ({
|
|
4
8
|
default: {
|
|
5
9
|
isMartyModeEnabled: false,
|
|
@@ -42,7 +46,7 @@ vi.mock('@/utils/lib', () => ({
|
|
|
42
46
|
gn: vi.fn(),
|
|
43
47
|
CSSTransition: vi.fn(),
|
|
44
48
|
localx: vi.fn(),
|
|
45
|
-
newHTML:
|
|
49
|
+
newHTML: newHTMLMock,
|
|
46
50
|
newButton: vi.fn(),
|
|
47
51
|
scaleMultiplier: 1,
|
|
48
52
|
fullscreenScaleMultiplier: 1,
|
|
@@ -75,18 +79,36 @@ vi.mock('@/html-svgs/sprite_toggle_on', () => ({ spriteToggleOn: '<svg></svg>' }
|
|
|
75
79
|
vi.mock('@/html-svgs/battery-svg', () => ({ batterySvg: () => '<svg id="battery"></svg>' }));
|
|
76
80
|
vi.mock('@/html-svgs/signal-svg', () => ({ signalSvg: () => '<svg id="signal"></svg>' }));
|
|
77
81
|
vi.mock('@/utils/raft-subscription-helpers', () => ({
|
|
78
|
-
createRaftConnectionIssueDetectedHelper:
|
|
79
|
-
subscribe: vi.fn(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
createRaftConnectionIssueDetectedHelper: raft => ({
|
|
83
|
+
subscribe: vi.fn(callback => {
|
|
84
|
+
raft.__connectionIssueDetectedCallback = callback;
|
|
85
|
+
}),
|
|
86
|
+
unsubscribe: vi.fn(() => {
|
|
87
|
+
raft.__connectionIssueDetectedUnsubscribed = true;
|
|
88
|
+
})
|
|
85
89
|
}),
|
|
86
|
-
|
|
87
|
-
subscribe: vi.fn(
|
|
88
|
-
|
|
90
|
+
createRaftConnectionIssueResolvedHelper: raft => ({
|
|
91
|
+
subscribe: vi.fn(callback => {
|
|
92
|
+
raft.__connectionIssueResolvedCallback = callback;
|
|
93
|
+
}),
|
|
94
|
+
unsubscribe: vi.fn(() => {
|
|
95
|
+
raft.__connectionIssueResolvedUnsubscribed = true;
|
|
96
|
+
})
|
|
89
97
|
}),
|
|
98
|
+
raftVerifiedSubscriptionHelper: raft => {
|
|
99
|
+
let callback;
|
|
100
|
+
return {
|
|
101
|
+
subscribe: vi.fn(nextCallback => {
|
|
102
|
+
callback = nextCallback;
|
|
103
|
+
raft.__verifiedCallbacks = raft.__verifiedCallbacks || [];
|
|
104
|
+
raft.__verifiedCallbacks.push(callback);
|
|
105
|
+
}),
|
|
106
|
+
unsubscribe: vi.fn(() => {
|
|
107
|
+
raft.__verifiedUnsubscribeCount = (raft.__verifiedUnsubscribeCount || 0) + 1;
|
|
108
|
+
raft.__verifiedCallbacks = (raft.__verifiedCallbacks || []).filter(item => item !== callback);
|
|
109
|
+
})
|
|
110
|
+
};
|
|
111
|
+
},
|
|
90
112
|
raftDisconnectedSubscriptionHelper: raft => {
|
|
91
113
|
const helper = {
|
|
92
114
|
subscribe: vi.fn(callback => {
|
|
@@ -128,6 +150,11 @@ describe('Marty connection UI', () => {
|
|
|
128
150
|
removeMarty: vi.fn(),
|
|
129
151
|
setMartySensorAvailability: vi.fn()
|
|
130
152
|
},
|
|
153
|
+
cogManager: {
|
|
154
|
+
addCog: vi.fn(),
|
|
155
|
+
wireCogWithBlocks: vi.fn(),
|
|
156
|
+
removeCog: vi.fn()
|
|
157
|
+
},
|
|
131
158
|
microBitManager: {
|
|
132
159
|
addMicroBit: vi.fn(),
|
|
133
160
|
wireMicroBitWithBlocks: vi.fn(),
|
|
@@ -139,6 +166,13 @@ describe('Marty connection UI', () => {
|
|
|
139
166
|
import('@/editor/ui/UI.js'),
|
|
140
167
|
import('@/editor/ScratchJr')
|
|
141
168
|
]);
|
|
169
|
+
newHTMLMock.mockImplementation((type, className, parent) => {
|
|
170
|
+
const element = new FakeElement(className ? [className] : []);
|
|
171
|
+
if (parent) {
|
|
172
|
+
parent.appendChild(element);
|
|
173
|
+
}
|
|
174
|
+
return element;
|
|
175
|
+
});
|
|
142
176
|
UI = uiModule.default;
|
|
143
177
|
ScratchJr = scratchJrModule.default;
|
|
144
178
|
ScratchJr.isMartyModeEnabled = false;
|
|
@@ -249,6 +283,102 @@ describe('Marty connection UI', () => {
|
|
|
249
283
|
expect(button.querySelector('.iconButtonContainer').classList.contains('connectedButtonContainer')).toBe(true);
|
|
250
284
|
});
|
|
251
285
|
|
|
286
|
+
it('uses one verified subscription instance and cleans it up after setup', () => {
|
|
287
|
+
const button = createConnectionButton();
|
|
288
|
+
const raft = createCogRaft();
|
|
289
|
+
const setupConnectionButton = vi.fn();
|
|
290
|
+
|
|
291
|
+
UI.setupConnectionButtonWhenVerified(button, raft, setupConnectionButton);
|
|
292
|
+
|
|
293
|
+
expect(raft.__verifiedCallbacks).toHaveLength(1);
|
|
294
|
+
const verifiedCallback = raft.__verifiedCallbacks[0];
|
|
295
|
+
raft.isVerified = true;
|
|
296
|
+
verifiedCallback();
|
|
297
|
+
verifiedCallback();
|
|
298
|
+
|
|
299
|
+
expect(setupConnectionButton).toHaveBeenCalledTimes(1);
|
|
300
|
+
expect(setupConnectionButton).toHaveBeenCalledWith(button, raft);
|
|
301
|
+
expect(raft.__verifiedUnsubscribeCount).toBe(1);
|
|
302
|
+
expect(raft.__verifiedCallbacks).toHaveLength(0);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('sets up an already verified Cog even when its verified event was missed', () => {
|
|
306
|
+
const button = createConnectionButton();
|
|
307
|
+
const raft = createCogRaft();
|
|
308
|
+
const setupConnectionButton = vi.fn();
|
|
309
|
+
raft.isVerified = true;
|
|
310
|
+
|
|
311
|
+
UI.setupConnectionButtonWhenVerified(button, raft, setupConnectionButton);
|
|
312
|
+
|
|
313
|
+
expect(setupConnectionButton).toHaveBeenCalledWith(button, raft);
|
|
314
|
+
expect(raft.__verifiedUnsubscribeCount).toBe(1);
|
|
315
|
+
expect(raft.__verifiedCallbacks).toHaveLength(0);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('reconciles an existing verified Cog after the host application manager is injected', () => {
|
|
319
|
+
const cogButton = createConnectionButton();
|
|
320
|
+
const martyButton = createConnectionButton();
|
|
321
|
+
const raft = createCogRaft();
|
|
322
|
+
raft.isVerified = true;
|
|
323
|
+
const setupCogSpy = vi.spyOn(UI, 'setupCogConnectionButton').mockImplementation(() => {});
|
|
324
|
+
window.applicationManager = undefined;
|
|
325
|
+
|
|
326
|
+
expect(UI.reconcileConnectionButtonsWhenApplicationManagerReady(cogButton, martyButton)).toBe(false);
|
|
327
|
+
window.applicationManager = {
|
|
328
|
+
getTheCurrentlySelectedDeviceOrFirstOfItsKind: vi.fn(type => type === 'Cog' ? raft : undefined)
|
|
329
|
+
};
|
|
330
|
+
vi.advanceTimersByTime(100);
|
|
331
|
+
|
|
332
|
+
expect(setupCogSpy).toHaveBeenCalledWith(cogButton, raft);
|
|
333
|
+
expect(raft.__verifiedUnsubscribeCount).toBe(1);
|
|
334
|
+
setupCogSpy.mockRestore();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('disconnects and clears a stale Cog when the connection issue countdown expires', () => {
|
|
338
|
+
const button = createConnectionButton();
|
|
339
|
+
const raft = createCogRaft();
|
|
340
|
+
window.applicationManager.disconnectGeneric = vi.fn((disconnectedRaft, onDisconnected) => {
|
|
341
|
+
onDisconnected();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
UI.setupCogConnectionButton(button, raft);
|
|
345
|
+
raft.__connectionIssueDetectedCallback();
|
|
346
|
+
|
|
347
|
+
expect(button.querySelector('.connIssueOverlay').textContent).toBe('A11Y_CONNECTION_LOST 60');
|
|
348
|
+
expect(button.style.pointerEvents).toBe('none');
|
|
349
|
+
|
|
350
|
+
vi.advanceTimersByTime(59000);
|
|
351
|
+
expect(window.applicationManager.disconnectGeneric).not.toHaveBeenCalled();
|
|
352
|
+
expect(button.querySelector('.connIssueOverlay').textContent).toBe('A11Y_CONNECTION_LOST 1');
|
|
353
|
+
|
|
354
|
+
vi.advanceTimersByTime(1000);
|
|
355
|
+
|
|
356
|
+
expect(window.applicationManager.disconnectGeneric).toHaveBeenCalledWith(raft, expect.any(Function), true);
|
|
357
|
+
expect(window.cogManager.removeCog).toHaveBeenCalledWith(raft);
|
|
358
|
+
expect(button.querySelector('.connIssueOverlay')).toBeNull();
|
|
359
|
+
expect(button.style.pointerEvents).toBe('auto');
|
|
360
|
+
expect(button.classList.contains('connectButtonConnected')).toBe(false);
|
|
361
|
+
expect(raft.__connectionIssueDetectedUnsubscribed).toBe(true);
|
|
362
|
+
expect(raft.__connectionIssueResolvedUnsubscribed).toBe(true);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('cancels the Cog countdown when the connection issue resolves', () => {
|
|
366
|
+
const button = createConnectionButton();
|
|
367
|
+
const raft = createCogRaft();
|
|
368
|
+
|
|
369
|
+
UI.setupCogConnectionButton(button, raft);
|
|
370
|
+
raft.__connectionIssueDetectedCallback();
|
|
371
|
+
vi.advanceTimersByTime(30000);
|
|
372
|
+
raft.__connectionIssueResolvedCallback();
|
|
373
|
+
vi.advanceTimersByTime(31000);
|
|
374
|
+
|
|
375
|
+
expect(window.applicationManager.disconnectGeneric).not.toHaveBeenCalled();
|
|
376
|
+
expect(button.querySelector('.connIssueOverlay')).toBeNull();
|
|
377
|
+
expect(button.style.pointerEvents).toBe('auto');
|
|
378
|
+
|
|
379
|
+
raft.__disconnectCallback();
|
|
380
|
+
});
|
|
381
|
+
|
|
252
382
|
it('uses a friendly message when the micro:bit chooser is cancelled', () => {
|
|
253
383
|
const message = UI.getMicroBitConnectionErrorMessage(
|
|
254
384
|
new DOMException('User cancelled the requestDevice() chooser.', 'NotFoundError')
|
|
@@ -293,6 +423,19 @@ function createMartyRaft() {
|
|
|
293
423
|
};
|
|
294
424
|
}
|
|
295
425
|
|
|
426
|
+
function createCogRaft() {
|
|
427
|
+
return {
|
|
428
|
+
id: 'cog-1',
|
|
429
|
+
isVerified: false,
|
|
430
|
+
hasVerifiedConnection() {
|
|
431
|
+
return this.isVerified;
|
|
432
|
+
},
|
|
433
|
+
getFriendlyName: () => 'Cog One',
|
|
434
|
+
getBatteryStrength: () => 70,
|
|
435
|
+
getRSSI: () => -45
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
296
439
|
function createConnectionButton() {
|
|
297
440
|
const button = new FakeElement();
|
|
298
441
|
button.children['.iconButtonContainer'] = new FakeElement(['iconButtonContainer', 'notConnectedButtonContainer']);
|
|
@@ -325,6 +468,7 @@ class FakeElement {
|
|
|
325
468
|
constructor(classes = []) {
|
|
326
469
|
this.classList = new FakeClassList(classes);
|
|
327
470
|
this.children = {};
|
|
471
|
+
this.childNodes = [];
|
|
328
472
|
this.style = {};
|
|
329
473
|
this.attributes = {};
|
|
330
474
|
this.textContent = '';
|
|
@@ -333,7 +477,28 @@ class FakeElement {
|
|
|
333
477
|
}
|
|
334
478
|
|
|
335
479
|
querySelector(selector) {
|
|
336
|
-
|
|
480
|
+
if (this.children[selector]) {
|
|
481
|
+
return this.children[selector];
|
|
482
|
+
}
|
|
483
|
+
if (selector.startsWith('.')) {
|
|
484
|
+
const className = selector.slice(1);
|
|
485
|
+
return this.childNodes.find(child => child.classList.contains(className)) || null;
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
appendChild(child) {
|
|
491
|
+
if (!this.childNodes.includes(child)) {
|
|
492
|
+
this.childNodes.push(child);
|
|
493
|
+
}
|
|
494
|
+
child.parentNode = this;
|
|
495
|
+
return child;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
removeChild(child) {
|
|
499
|
+
this.childNodes = this.childNodes.filter(item => item !== child);
|
|
500
|
+
child.parentNode = null;
|
|
501
|
+
return child;
|
|
337
502
|
}
|
|
338
503
|
|
|
339
504
|
setAttribute(name, value) {
|