@frockbot/plugin-shell 0.3.21 → 0.3.23
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/package.json +32 -32
- package/src/backend-applets.test.ts +38 -0
- package/src/backend-applets.ts +32 -2
- package/src/backend-recovery-integration.test.ts +134 -0
- package/src/backend.ts +98 -44
- package/src/client/AppletCanvas.vue +128 -30
- package/src/client/FrockBotApp.vue +111 -2
- package/src/client/applet-building-view.test.ts +69 -0
- package/src/client/applet-progress.test.ts +300 -0
- package/src/client/applet-progress.ts +339 -0
- package/src/client/deployment.test.ts +152 -0
- package/src/client/deployment.ts +138 -0
- package/src/client/index.test.ts +71 -0
- package/src/client/index.ts +58 -0
- package/src/client/styles.css +91 -4
- package/src/run-protocol.ts +10 -0
- package/src/shared.ts +14 -0
|
@@ -22,6 +22,11 @@ import {
|
|
|
22
22
|
appletSourceFingerprintV1,
|
|
23
23
|
mostRecentlyChangedFileV1,
|
|
24
24
|
} from "./applets-client.js";
|
|
25
|
+
import {
|
|
26
|
+
appletIsBeingBuiltV1,
|
|
27
|
+
appletProgressToolsV1,
|
|
28
|
+
appletProgressV1,
|
|
29
|
+
} from "./applet-progress.js";
|
|
25
30
|
import { packageIframePagesForSlotV1 } from "./package-iframe-entries.js";
|
|
26
31
|
import PackageIframeHost from "./PackageIframeHost.vue";
|
|
27
32
|
|
|
@@ -46,6 +51,26 @@ const isRunning = computed(() => Boolean(web.value.activeRunId));
|
|
|
46
51
|
const canvasState = computed(() => web.value.appletCanvas);
|
|
47
52
|
const failure = computed(() => web.value.appletCanvasError);
|
|
48
53
|
|
|
54
|
+
/*
|
|
55
|
+
* What the Bot is doing to this Applet, in one line.
|
|
56
|
+
*
|
|
57
|
+
* A publish is minutes away from a create, and for all of it there is nothing
|
|
58
|
+
* to run. The panel used to answer that with one fixed line about the Applet
|
|
59
|
+
* not being live and a file list, which never changed and never said whether
|
|
60
|
+
* anything was happening. This is the sentence in between, projected from what
|
|
61
|
+
* the thread already shows: no new read, no new durable state.
|
|
62
|
+
*/
|
|
63
|
+
const progress = computed(() =>
|
|
64
|
+
appletProgressV1({
|
|
65
|
+
applet: applet.value ?? null,
|
|
66
|
+
source: source.value,
|
|
67
|
+
build: build.value,
|
|
68
|
+
tools: appletProgressToolsV1(web.value.messages),
|
|
69
|
+
running: isRunning.value,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
const beingBuilt = computed(() => appletIsBeingBuiltV1(progress.value));
|
|
73
|
+
|
|
49
74
|
/** The Applets Package's right-panel page, when its Composition carries one. */
|
|
50
75
|
const panelPage = computed(
|
|
51
76
|
() => packageIframePagesForSlotV1(web.value.packageUi, RIGHT_PANEL_SLOT)[0],
|
|
@@ -212,8 +237,11 @@ async function clearFocus(): Promise<void> {
|
|
|
212
237
|
/></span>
|
|
213
238
|
<div class="applet-canvas-title">
|
|
214
239
|
<strong>{{ applet?.displayName ?? "Applet" }}</strong>
|
|
240
|
+
<!--
|
|
241
|
+
While it is being built the line under the name is the pinned
|
|
242
|
+
building view below, not a copy of it here.
|
|
243
|
+
-->
|
|
215
244
|
<small v-if="viewer">{{ generationLabel(viewer.generationId) }}</small>
|
|
216
|
-
<small v-else>Not published yet</small>
|
|
217
245
|
</div>
|
|
218
246
|
<div
|
|
219
247
|
v-if="canShowApp"
|
|
@@ -255,6 +283,39 @@ async function clearFocus(): Promise<void> {
|
|
|
255
283
|
/>
|
|
256
284
|
</header>
|
|
257
285
|
|
|
286
|
+
<!--
|
|
287
|
+
Where the work has got to, pinned under the header while the Applet is
|
|
288
|
+
still being built: one line a person can read, the reason it stopped when
|
|
289
|
+
it stopped, and the tail of whatever the check or the build printed —
|
|
290
|
+
which is the only thing that helps when the Bot is going round in circles
|
|
291
|
+
on a type error. It sits outside the scrolling column deliberately: a
|
|
292
|
+
person reading the source still wants to know what is happening to it.
|
|
293
|
+
-->
|
|
294
|
+
<section
|
|
295
|
+
v-if="applet && !loading && progress && beingBuilt"
|
|
296
|
+
class="applet-canvas-progress"
|
|
297
|
+
:class="{ 'applet-canvas-progress-wrong': Boolean(progress.failure) }"
|
|
298
|
+
data-testid="applet-canvas-progress"
|
|
299
|
+
role="status"
|
|
300
|
+
>
|
|
301
|
+
<p class="applet-canvas-progress-line">
|
|
302
|
+
<span
|
|
303
|
+
v-if="progress.working"
|
|
304
|
+
class="applet-canvas-progress-dot"
|
|
305
|
+
aria-hidden="true"
|
|
306
|
+
/>
|
|
307
|
+
<UiIcon v-else-if="progress.failure" name="close" size="sm" />
|
|
308
|
+
<span>{{ progress.label }}</span>
|
|
309
|
+
</p>
|
|
310
|
+
<p v-if="progress.failure" class="applet-canvas-progress-failure">
|
|
311
|
+
{{ progress.failure }}
|
|
312
|
+
</p>
|
|
313
|
+
<pre
|
|
314
|
+
v-if="progress.output"
|
|
315
|
+
class="applet-canvas-progress-output"
|
|
316
|
+
><code>{{ progress.output.join("\n") }}</code></pre>
|
|
317
|
+
</section>
|
|
318
|
+
|
|
258
319
|
<div class="applet-canvas-body">
|
|
259
320
|
<!-- Loading: the shape of what is coming, with a caption that says so. -->
|
|
260
321
|
<div v-if="loading" class="applet-canvas-loading">
|
|
@@ -297,21 +358,6 @@ async function clearFocus(): Promise<void> {
|
|
|
297
358
|
<span>This is taking longer than it should.</span>
|
|
298
359
|
<button type="button" @click="retry">Try again</button>
|
|
299
360
|
</div>
|
|
300
|
-
<p
|
|
301
|
-
v-if="build && build.status !== 'unknown'"
|
|
302
|
-
class="applet-canvas-build"
|
|
303
|
-
:class="`applet-canvas-build-${build.status}`"
|
|
304
|
-
>
|
|
305
|
-
<UiIcon
|
|
306
|
-
:name="build.status === 'passed' ? 'check' : 'close'"
|
|
307
|
-
size="sm"
|
|
308
|
-
/>
|
|
309
|
-
<span
|
|
310
|
-
>{{ build.command === "build" ? "Build" : "Check" }}
|
|
311
|
-
{{ build.status
|
|
312
|
-
}}{{ build.summary ? `: ${build.summary}` : "" }}</span
|
|
313
|
-
>
|
|
314
|
-
</p>
|
|
315
361
|
<div v-if="sortedFiles.length > 0" class="applet-canvas-files">
|
|
316
362
|
<button
|
|
317
363
|
v-for="file in sortedFiles"
|
|
@@ -573,28 +619,76 @@ async function clearFocus(): Promise<void> {
|
|
|
573
619
|
white-space: pre;
|
|
574
620
|
}
|
|
575
621
|
|
|
576
|
-
.
|
|
622
|
+
/* Where the work has got to: the panel's own header for the building state. */
|
|
623
|
+
.applet-canvas-progress {
|
|
624
|
+
display: flex;
|
|
625
|
+
flex: 0 0 auto;
|
|
626
|
+
flex-direction: column;
|
|
627
|
+
gap: 8px;
|
|
628
|
+
padding: 10px 12px;
|
|
629
|
+
border-bottom: 1px solid var(--frock-border);
|
|
630
|
+
background: var(--frock-surface-subtle);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.applet-canvas-progress-wrong {
|
|
634
|
+
border-bottom-color: var(--frock-danger-border);
|
|
635
|
+
background: var(--frock-danger-surface);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.applet-canvas-progress-line {
|
|
577
639
|
display: flex;
|
|
578
640
|
align-items: center;
|
|
579
|
-
gap:
|
|
641
|
+
gap: 8px;
|
|
580
642
|
margin: 0;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
643
|
+
color: var(--frock-text);
|
|
644
|
+
font-size: var(--frock-text-sm);
|
|
645
|
+
font-weight: 600;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.applet-canvas-progress-wrong .applet-canvas-progress-line {
|
|
649
|
+
color: var(--frock-danger-text);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/* The same "something is happening" the thread uses, at the size of a word. */
|
|
653
|
+
.applet-canvas-progress-dot {
|
|
654
|
+
width: 8px;
|
|
655
|
+
height: 8px;
|
|
656
|
+
flex: 0 0 auto;
|
|
657
|
+
border-radius: 999px;
|
|
658
|
+
background: var(--frock-action-primary);
|
|
659
|
+
animation: applet-progress-pulse 1.4s ease-in-out infinite;
|
|
586
660
|
}
|
|
587
661
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
662
|
+
@keyframes applet-progress-pulse {
|
|
663
|
+
0%,
|
|
664
|
+
100% {
|
|
665
|
+
opacity: 0.35;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
50% {
|
|
669
|
+
opacity: 1;
|
|
670
|
+
}
|
|
592
671
|
}
|
|
593
672
|
|
|
594
|
-
.applet-canvas-
|
|
595
|
-
|
|
673
|
+
.applet-canvas-progress-failure {
|
|
674
|
+
margin: 0;
|
|
596
675
|
color: var(--frock-danger-text);
|
|
597
|
-
|
|
676
|
+
font-size: var(--frock-text-sm);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
.applet-canvas-progress-output {
|
|
680
|
+
max-height: 180px;
|
|
681
|
+
margin: 0;
|
|
682
|
+
overflow: auto;
|
|
683
|
+
padding: 8px 10px;
|
|
684
|
+
border: 1px solid var(--frock-border);
|
|
685
|
+
border-radius: var(--frock-radius-control);
|
|
686
|
+
color: var(--frock-text-muted);
|
|
687
|
+
background: var(--frock-surface);
|
|
688
|
+
font-family: var(--frock-font-mono);
|
|
689
|
+
font-size: var(--frock-text-xs);
|
|
690
|
+
line-height: var(--frock-leading-snug);
|
|
691
|
+
white-space: pre;
|
|
598
692
|
}
|
|
599
693
|
|
|
600
694
|
.applet-canvas-failure {
|
|
@@ -686,5 +780,9 @@ async function clearFocus(): Promise<void> {
|
|
|
686
780
|
.applet-app-leave-active {
|
|
687
781
|
transition: none;
|
|
688
782
|
}
|
|
783
|
+
|
|
784
|
+
.applet-canvas-progress-dot {
|
|
785
|
+
animation: none;
|
|
786
|
+
}
|
|
689
787
|
}
|
|
690
788
|
</style>
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
} from "./turn-limits.js";
|
|
57
57
|
import SendPayloadView from "./SendPayloadView.vue";
|
|
58
58
|
import AppletCanvas from "./AppletCanvas.vue";
|
|
59
|
+
import { appletProgressToolsV1, appletProgressV1 } from "./applet-progress.js";
|
|
59
60
|
import PackageIframeHost from "./PackageIframeHost.vue";
|
|
60
61
|
import type { ClientSkillCatalogEntryV1 } from "../skill-protocol.js";
|
|
61
62
|
import {
|
|
@@ -67,6 +68,14 @@ import {
|
|
|
67
68
|
textWithoutSkillTriggerV1,
|
|
68
69
|
type SkillPopoverStateV1,
|
|
69
70
|
} from "./skill-invocation.js";
|
|
71
|
+
import {
|
|
72
|
+
DEPLOYMENT_RELOAD_LABEL_V1,
|
|
73
|
+
DEPLOYMENT_UPDATED_MESSAGE_V1,
|
|
74
|
+
deploymentFollowV1,
|
|
75
|
+
deploymentReloadStoreV1,
|
|
76
|
+
readDeploymentReloadV1,
|
|
77
|
+
writeDeploymentReloadV1,
|
|
78
|
+
} from "./deployment.js";
|
|
70
79
|
|
|
71
80
|
const injectedWeb = inject(frockBotWebDataKey);
|
|
72
81
|
if (!injectedWeb) throw new Error("shell client data was not provided");
|
|
@@ -264,6 +273,25 @@ const appletChip = computed(() =>
|
|
|
264
273
|
? (state.value.focusedApplet?.displayName ?? "Applet")
|
|
265
274
|
: undefined,
|
|
266
275
|
);
|
|
276
|
+
/*
|
|
277
|
+
* What the Bot is doing to it, on the phone.
|
|
278
|
+
*
|
|
279
|
+
* There is no room beside the conversation here, so the chip carries the line
|
|
280
|
+
* the canvas would have shown and opens the canvas over the whole screen. It
|
|
281
|
+
* is the same projection and the same words: a person who moves between their
|
|
282
|
+
* phone and a laptop reads one story, not two.
|
|
283
|
+
*/
|
|
284
|
+
const appletChipStatus = computed(() => {
|
|
285
|
+
if (!appletChip.value) return undefined;
|
|
286
|
+
const progress = appletProgressV1({
|
|
287
|
+
applet: state.value.focusedApplet ?? null,
|
|
288
|
+
source: state.value.appletSource,
|
|
289
|
+
build: state.value.appletBuild,
|
|
290
|
+
tools: appletProgressToolsV1(state.value.messages),
|
|
291
|
+
running: Boolean(state.value.activeRunId),
|
|
292
|
+
});
|
|
293
|
+
return progress && progress.stage !== "published" ? progress : undefined;
|
|
294
|
+
});
|
|
267
295
|
/*
|
|
268
296
|
* Skill invocation. `/` or `@` at a word boundary opens a popover over the
|
|
269
297
|
* Bot's catalog; choosing one attaches a ref chip and removes the trigger from
|
|
@@ -411,6 +439,57 @@ let voiceTail = "";
|
|
|
411
439
|
let voiceSendOnFinal = false;
|
|
412
440
|
|
|
413
441
|
const dictating = computed(() => voiceState.value !== "idle");
|
|
442
|
+
|
|
443
|
+
/*
|
|
444
|
+
* Following a release.
|
|
445
|
+
*
|
|
446
|
+
* A reload is destructive, so the shell only does it on its own when there is
|
|
447
|
+
* nothing to lose, and otherwise says so and waits. The bar is not an alert:
|
|
448
|
+
* nothing is wrong, there is just newer code to pick up.
|
|
449
|
+
*/
|
|
450
|
+
const reloadStore = deploymentReloadStoreV1();
|
|
451
|
+
const lastReloadedAt = readDeploymentReloadV1(reloadStore);
|
|
452
|
+
const updateBarVisible = ref(false);
|
|
453
|
+
|
|
454
|
+
function followDeployment(): void {
|
|
455
|
+
const decision = deploymentFollowV1({
|
|
456
|
+
stale: state.value.deploymentStale,
|
|
457
|
+
turnRunning: Boolean(state.value.runningRunId),
|
|
458
|
+
draft: draft.value,
|
|
459
|
+
overlayOpen: Boolean(overlaySurface.value),
|
|
460
|
+
listening: dictating.value,
|
|
461
|
+
holds: state.value.reloadHolds,
|
|
462
|
+
now: Date.now(),
|
|
463
|
+
...(lastReloadedAt === undefined ? {} : { reloadedAt: lastReloadedAt }),
|
|
464
|
+
});
|
|
465
|
+
updateBarVisible.value = decision === "offer";
|
|
466
|
+
if (decision !== "reload") return;
|
|
467
|
+
reloadNow();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function reloadNow(): void {
|
|
471
|
+
writeDeploymentReloadV1(reloadStore, Date.now());
|
|
472
|
+
/*
|
|
473
|
+
* `location.reload()` and nothing else. The Android shell runs this very
|
|
474
|
+
* bundle inside a WebView served from its own origin, so re-navigating to a
|
|
475
|
+
* URL this code built would leave that origin behind; reloading in place
|
|
476
|
+
* works the same way in both.
|
|
477
|
+
*/
|
|
478
|
+
window.location.reload();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
watch(
|
|
482
|
+
[
|
|
483
|
+
() => state.value.deploymentStale,
|
|
484
|
+
() => state.value.runningRunId,
|
|
485
|
+
() => state.value.reloadHolds,
|
|
486
|
+
draft,
|
|
487
|
+
overlaySurface,
|
|
488
|
+
dictating,
|
|
489
|
+
],
|
|
490
|
+
() => followDeployment(),
|
|
491
|
+
{ immediate: true },
|
|
492
|
+
);
|
|
414
493
|
const voiceButtonLabel = computed(() => voiceButtonLabelV1(voiceState.value));
|
|
415
494
|
/**
|
|
416
495
|
* The wave button takes the slot only when the slot is otherwise idle: an
|
|
@@ -1574,6 +1653,23 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
1574
1653
|
</div>
|
|
1575
1654
|
</Transition>
|
|
1576
1655
|
|
|
1656
|
+
<Transition name="banner">
|
|
1657
|
+
<!--
|
|
1658
|
+
One bar at a time in this spot. A failed Turn is the more urgent
|
|
1659
|
+
thing to read, and newer code is still there once it is dealt with.
|
|
1660
|
+
-->
|
|
1661
|
+
<div
|
|
1662
|
+
v-if="updateBarVisible && !state.error && !state.activeRun"
|
|
1663
|
+
class="update-banner"
|
|
1664
|
+
role="status"
|
|
1665
|
+
>
|
|
1666
|
+
<span>{{ DEPLOYMENT_UPDATED_MESSAGE_V1 }}</span>
|
|
1667
|
+
<button type="button" @click="reloadNow()">
|
|
1668
|
+
{{ DEPLOYMENT_RELOAD_LABEL_V1 }}
|
|
1669
|
+
</button>
|
|
1670
|
+
</div>
|
|
1671
|
+
</Transition>
|
|
1672
|
+
|
|
1577
1673
|
<!--
|
|
1578
1674
|
No Bot, no composer. A disabled input under a made-up Bot name reads
|
|
1579
1675
|
as a broken Bot; the first-run pane above points at making one.
|
|
@@ -1618,11 +1714,24 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
1618
1714
|
v-if="appletChip"
|
|
1619
1715
|
type="button"
|
|
1620
1716
|
class="applet-chip"
|
|
1717
|
+
data-testid="applet-chip"
|
|
1621
1718
|
@click="toggleRightPanel"
|
|
1622
1719
|
>
|
|
1623
1720
|
<UiIcon name="applets" size="sm" />
|
|
1624
|
-
<span class="applet-chip-
|
|
1625
|
-
|
|
1721
|
+
<span class="applet-chip-text">
|
|
1722
|
+
<span class="applet-chip-name">Applet: {{ appletChip }}</span>
|
|
1723
|
+
<span v-if="appletChipStatus" class="applet-chip-status">
|
|
1724
|
+
<span
|
|
1725
|
+
v-if="appletChipStatus.working"
|
|
1726
|
+
class="applet-chip-dot"
|
|
1727
|
+
aria-hidden="true"
|
|
1728
|
+
/>
|
|
1729
|
+
{{ appletChipStatus.failure ?? appletChipStatus.label }}
|
|
1730
|
+
</span>
|
|
1731
|
+
</span>
|
|
1732
|
+
<span class="applet-chip-action">{{
|
|
1733
|
+
appletChipStatus ? "Watch" : "Open"
|
|
1734
|
+
}}</span>
|
|
1626
1735
|
</button>
|
|
1627
1736
|
<div class="composer-body">
|
|
1628
1737
|
<ul v-if="attachedSkills.length > 0" class="skill-chips">
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The building view, as the two surfaces that draw it are written.
|
|
3
|
+
*
|
|
4
|
+
* Bun has no single-file-component loader, so a `.vue` file is read as text
|
|
5
|
+
* here — the same thing `ComputerCard.test.ts` does. It proves the wiring the
|
|
6
|
+
* unit tests for the projection cannot: that the panel and the phone chip both
|
|
7
|
+
* read the one projection, and that neither still shows the fixed line it used
|
|
8
|
+
* to show for the whole of a build.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, test } from "bun:test";
|
|
11
|
+
import { parse } from "@vue/compiler-sfc";
|
|
12
|
+
|
|
13
|
+
const canvas = await Bun.file(
|
|
14
|
+
new URL("./AppletCanvas.vue", import.meta.url),
|
|
15
|
+
).text();
|
|
16
|
+
const app = await Bun.file(
|
|
17
|
+
new URL("./FrockBotApp.vue", import.meta.url),
|
|
18
|
+
).text();
|
|
19
|
+
const styles = await Bun.file(new URL("./styles.css", import.meta.url)).text();
|
|
20
|
+
|
|
21
|
+
describe("the Applet canvas while the Bot is still building", () => {
|
|
22
|
+
test("the file parses as a single-file component", () => {
|
|
23
|
+
expect(parse(canvas).errors).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("the panel draws the projection rather than a fixed line", () => {
|
|
27
|
+
expect(canvas).toContain('from "./applet-progress.js"');
|
|
28
|
+
expect(canvas).toContain('data-testid="applet-canvas-progress"');
|
|
29
|
+
expect(canvas).toContain("progress.label");
|
|
30
|
+
expect(canvas).toContain("progress.failure");
|
|
31
|
+
expect(canvas).toContain("progress.output");
|
|
32
|
+
// The line the panel used to show for minutes on end, whatever happened.
|
|
33
|
+
expect(canvas).not.toContain("Not published yet");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("the progress block is announced and is gone once the Applet runs", () => {
|
|
37
|
+
expect(canvas).toContain('role="status"');
|
|
38
|
+
expect(canvas).toContain(
|
|
39
|
+
'v-if="applet && !loading && progress && beingBuilt"',
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("the build output is monospace and cannot grow without bound", () => {
|
|
44
|
+
expect(canvas).toContain("font-family: var(--frock-font-mono);");
|
|
45
|
+
expect(canvas).toContain("max-height: 180px;");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("the working dot stops moving where motion is not wanted", () => {
|
|
49
|
+
expect(canvas).toContain("@media (prefers-reduced-motion: reduce)");
|
|
50
|
+
expect(canvas).toContain(
|
|
51
|
+
".applet-canvas-progress-dot {\n animation: none;",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("the phone's way in", () => {
|
|
57
|
+
test("the chip carries the same line and opens the same panel", () => {
|
|
58
|
+
expect(app).toContain('from "./applet-progress.js"');
|
|
59
|
+
expect(app).toContain("appletChipStatus");
|
|
60
|
+
// The one overlay: the right panel drawer, opened by the one toggle.
|
|
61
|
+
expect(app).toContain('class="applet-chip"');
|
|
62
|
+
expect(app).toContain('@click="toggleRightPanel"');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("the chip has room for a second line", () => {
|
|
66
|
+
expect(styles).toContain(".applet-chip-status {");
|
|
67
|
+
expect(styles).toContain("min-height: var(--frock-control-md);");
|
|
68
|
+
});
|
|
69
|
+
});
|