@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: flex;
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: 1fr 1fr 1fr;
111
- grid-template-columns: 1fr;
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: 1px;
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: -10px;
146
- left: -10px;
147
- width: 30px;
148
- height: 30px;
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
- .connectButton:hover .connectedButtonContainer {
152
- background-image: url('../assets/connection/disconnect_btn-hover.svg');
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: center;
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
- text-align: center;
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
- margin-bottom: ${css_vh(0.35)};
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@robotical/martyblocksjr",
3
- "version": "4.2.3",
3
+ "version": "4.2.5",
4
4
  "description": "ScratchJr",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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: vi.fn(),
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
- unsubscribe: vi.fn()
81
- }),
82
- createRaftConnectionIssueResolvedHelper: () => ({
83
- subscribe: vi.fn(),
84
- unsubscribe: vi.fn()
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
- raftVerifiedSubscriptionHelper: () => ({
87
- subscribe: vi.fn(),
88
- unsubscribe: vi.fn()
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
- return this.children[selector] || null;
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) {