pinokiod 7.5.40 → 7.5.41

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.5.40",
3
+ "version": "7.5.41",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -1498,6 +1498,28 @@ class Server {
1498
1498
  community_url = ""
1499
1499
  }
1500
1500
  }
1501
+ const buildGitRemoteWebUrl = (value) => {
1502
+ const raw = typeof value === "string" ? value.trim() : ""
1503
+ if (!raw) {
1504
+ return ""
1505
+ }
1506
+ if (/^https?:\/\//i.test(raw)) {
1507
+ return raw.replace(/\.git$/i, "")
1508
+ }
1509
+ const sshUrlMatch = raw.match(/^ssh:\/\/(?:[^@/]+@)?([^/]+)\/(.+?)(?:\.git)?$/i)
1510
+ if (sshUrlMatch && sshUrlMatch[1] && sshUrlMatch[2]) {
1511
+ return `https://${sshUrlMatch[1]}/${sshUrlMatch[2]}`
1512
+ }
1513
+ const scpMatch = raw.match(/^(?:[^@]+@)?([^:/]+):(.+?)(?:\.git)?$/)
1514
+ if (scpMatch && scpMatch[1] && scpMatch[2]) {
1515
+ return `https://${scpMatch[1]}/${scpMatch[2]}`
1516
+ }
1517
+ return ""
1518
+ }
1519
+ const launcherRemote = this.kernel.api && typeof this.kernel.api.parentGitURI === "function"
1520
+ ? (this.kernel.api.parentGitURI(this.kernel.path("api", name)) || "")
1521
+ : ""
1522
+ const launcherRemoteUrl = buildGitRemoteWebUrl(launcherRemote)
1501
1523
 
1502
1524
  let editor_tab = `/pinokio/fileview/${encodeURIComponent(name)}`
1503
1525
  const tabsStorageKey = `${name}:${type}`
@@ -1593,6 +1615,7 @@ class Server {
1593
1615
  review_tab,
1594
1616
  files_tab,
1595
1617
  community_url,
1618
+ launcher_remote_url: launcherRemoteUrl,
1596
1619
  // paths,
1597
1620
  theme: this.theme,
1598
1621
  agent: req.agent,
@@ -70,6 +70,173 @@ body.dark .app-header-identity {
70
70
  0 1px 2px rgba(2, 6, 23, 0.35);
71
71
  color: rgba(248, 250, 252, 0.94);
72
72
  }
73
+ .app-header-info {
74
+ position: relative;
75
+ flex: 0 1 auto;
76
+ min-width: 0;
77
+ display: inline-flex;
78
+ align-items: center;
79
+ pointer-events: auto;
80
+ }
81
+ .app-header-info-trigger {
82
+ appearance: none;
83
+ border: 0;
84
+ background: transparent;
85
+ color: inherit;
86
+ display: inline-flex;
87
+ align-items: center;
88
+ gap: 8px;
89
+ min-width: 0;
90
+ max-width: min(260px, 32vw);
91
+ height: 22px;
92
+ padding: 0 4px;
93
+ margin: 0 -4px;
94
+ border-radius: 6px;
95
+ font: inherit;
96
+ line-height: 1;
97
+ cursor: pointer;
98
+ transition: background 0.15s ease, color 0.15s ease;
99
+ }
100
+ .app-header-info-trigger:hover,
101
+ .app-header-info-trigger[aria-expanded="true"] {
102
+ background: rgba(15, 23, 42, 0.055);
103
+ }
104
+ body.dark .app-header-info-trigger:hover,
105
+ body.dark .app-header-info-trigger[aria-expanded="true"] {
106
+ background: rgba(255, 255, 255, 0.07);
107
+ }
108
+ .app-header-info-trigger:focus-visible {
109
+ outline: 2px solid rgba(10, 132, 255, 0.55);
110
+ outline-offset: 4px;
111
+ }
112
+ .app-header-info-popover {
113
+ position: absolute;
114
+ top: calc(100% + 9px);
115
+ left: 0;
116
+ width: min(390px, calc(100vw - 24px));
117
+ padding: 12px;
118
+ border: 1px solid rgba(15, 23, 42, 0.1);
119
+ border-radius: 10px;
120
+ background: rgba(255, 255, 255, 0.98);
121
+ box-shadow: 0 16px 42px rgba(15, 23, 42, 0.14);
122
+ color: #0f172a;
123
+ z-index: 10000020;
124
+ box-sizing: border-box;
125
+ }
126
+ body.dark .app-header-info-popover {
127
+ border-color: rgba(255, 255, 255, 0.1);
128
+ background: rgba(20, 22, 28, 0.98);
129
+ box-shadow: 0 18px 46px rgba(0, 0, 0, 0.48);
130
+ color: rgba(248, 250, 252, 0.94);
131
+ }
132
+ .app-header-info-popover.hidden {
133
+ display: none;
134
+ }
135
+ .app-header-info-popover-head {
136
+ margin-bottom: 10px;
137
+ color: rgba(71, 85, 105, 0.72);
138
+ font-size: 11px;
139
+ font-weight: 700;
140
+ line-height: 1;
141
+ text-transform: uppercase;
142
+ letter-spacing: 0.03em;
143
+ }
144
+ body.dark .app-header-info-popover-head {
145
+ color: rgba(203, 213, 225, 0.7);
146
+ }
147
+ .app-header-info-popover-main {
148
+ display: flex;
149
+ align-items: flex-start;
150
+ gap: 12px;
151
+ min-width: 0;
152
+ }
153
+ .app-header-info-popover-icon {
154
+ width: 58px;
155
+ height: 58px;
156
+ flex: 0 0 58px;
157
+ display: block;
158
+ object-fit: contain;
159
+ border-radius: 8px;
160
+ border: 1px solid rgba(15, 23, 42, 0.08);
161
+ background: rgba(248, 250, 252, 0.8);
162
+ }
163
+ body.dark .app-header-info-popover-icon {
164
+ border-color: rgba(255, 255, 255, 0.09);
165
+ background: rgba(255, 255, 255, 0.055);
166
+ }
167
+ .app-header-info-popover-copy {
168
+ min-width: 0;
169
+ flex: 1 1 auto;
170
+ }
171
+ .app-header-info-popover-title {
172
+ color: rgba(15, 23, 42, 0.92);
173
+ font-size: 15px;
174
+ font-weight: 700;
175
+ line-height: 1.2;
176
+ overflow-wrap: anywhere;
177
+ }
178
+ body.dark .app-header-info-popover-title {
179
+ color: rgba(248, 250, 252, 0.94);
180
+ }
181
+ .app-header-info-popover-description {
182
+ margin-top: 5px;
183
+ color: rgba(71, 85, 105, 0.82);
184
+ font-size: 12px;
185
+ font-weight: 500;
186
+ line-height: 1.4;
187
+ overflow-wrap: anywhere;
188
+ }
189
+ body.dark .app-header-info-popover-description {
190
+ color: rgba(203, 213, 225, 0.78);
191
+ }
192
+ .app-header-info-popover-link,
193
+ .app-header-info-popover-link--muted {
194
+ margin-top: 12px;
195
+ min-height: 32px;
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: space-between;
199
+ gap: 10px;
200
+ padding: 0 10px;
201
+ border-radius: 7px;
202
+ border: 1px solid rgba(15, 23, 42, 0.1);
203
+ background: rgba(15, 23, 42, 0.035);
204
+ color: rgba(15, 23, 42, 0.88);
205
+ font-size: 12px;
206
+ font-weight: 650;
207
+ line-height: 1.2;
208
+ text-decoration: none;
209
+ box-sizing: border-box;
210
+ }
211
+ .app-header-info-popover-link:hover {
212
+ background: rgba(15, 23, 42, 0.07);
213
+ }
214
+ .app-header-info-popover-link span {
215
+ min-width: 0;
216
+ overflow: hidden;
217
+ text-overflow: ellipsis;
218
+ white-space: nowrap;
219
+ }
220
+ .app-header-info-popover-link:focus-visible {
221
+ outline: 2px solid rgba(10, 132, 255, 0.55);
222
+ outline-offset: 2px;
223
+ }
224
+ body.dark .app-header-info-popover-link,
225
+ body.dark .app-header-info-popover-link--muted {
226
+ border-color: rgba(255, 255, 255, 0.1);
227
+ background: rgba(255, 255, 255, 0.06);
228
+ color: rgba(248, 250, 252, 0.9);
229
+ }
230
+ body.dark .app-header-info-popover-link:hover {
231
+ background: rgba(255, 255, 255, 0.09);
232
+ }
233
+ .app-header-info-popover-link--muted {
234
+ justify-content: flex-start;
235
+ color: rgba(100, 116, 139, 0.78);
236
+ }
237
+ body.dark .app-header-info-popover-link--muted {
238
+ color: rgba(148, 163, 184, 0.86);
239
+ }
73
240
  .app-header-identity-icon {
74
241
  width: 18px;
75
242
  height: 18px;
@@ -83,6 +250,9 @@ body.dark .app-header-identity {
83
250
  text-overflow: ellipsis;
84
251
  white-space: nowrap;
85
252
  }
253
+ .app-header-info-trigger .app-header-identity-title {
254
+ flex: 1 1 auto;
255
+ }
86
256
  .app-header-identity-separator {
87
257
  width: 3px;
88
258
  height: 3px;
@@ -6699,12 +6869,35 @@ header.navheader h1 {
6699
6869
  border: 1px solid rgba(0,0,0,0.08);
6700
6870
  background: rgba(255, 255, 255, 0.78);
6701
6871
  box-sizing: border-box;
6702
- overflow: hidden;
6872
+ overflow: visible;
6703
6873
  }
6704
6874
  body.dark .mobile-bottom-nav__identity {
6705
6875
  border-color: rgba(255,255,255,0.08);
6706
6876
  background: rgba(255,255,255,0.06);
6707
6877
  }
6878
+ .mobile-bottom-nav .app-header-info {
6879
+ flex: 0 1 42%;
6880
+ max-width: 42%;
6881
+ }
6882
+ .mobile-bottom-nav .app-header-info-trigger {
6883
+ width: 100%;
6884
+ max-width: 100%;
6885
+ height: auto;
6886
+ min-height: 18px;
6887
+ gap: 5px;
6888
+ padding: 0;
6889
+ margin: 0;
6890
+ }
6891
+ .mobile-bottom-nav .app-header-info-trigger:hover,
6892
+ .mobile-bottom-nav .app-header-info-trigger[aria-expanded="true"] {
6893
+ background: transparent;
6894
+ }
6895
+ .mobile-bottom-nav .app-header-info-popover {
6896
+ top: auto;
6897
+ bottom: calc(100% + 9px);
6898
+ left: -46px;
6899
+ width: min(390px, calc(100vw - 24px));
6900
+ }
6708
6901
  .mobile-bottom-nav__identity-icon {
6709
6902
  width: 16px;
6710
6903
  height: 16px;
@@ -6714,7 +6907,7 @@ header.navheader h1 {
6714
6907
  }
6715
6908
  .mobile-bottom-nav__identity-title {
6716
6909
  min-width: 0;
6717
- max-width: 42%;
6910
+ max-width: 100%;
6718
6911
  overflow: hidden;
6719
6912
  text-overflow: ellipsis;
6720
6913
  white-space: nowrap;
@@ -7864,6 +8057,10 @@ body.dark .pinokio-custom-terminal-header {
7864
8057
  })()
7865
8058
  </script>
7866
8059
  </head>
8060
+ <%
8061
+ const appInfoRemoteUrl = typeof launcher_remote_url === "string" ? launcher_remote_url : ""
8062
+ const appInfoRemoteDisplayUrl = appInfoRemoteUrl.replace(/^https?:\/\//i, "")
8063
+ %>
7867
8064
  <body class='<%= [theme, "app-page"].filter(Boolean).join(" ") %>' data-platform="<%=platform%>" data-agent="<%=agent%>" data-view="<%=type%>">
7868
8065
  <header class='navheader grabbable'>
7869
8066
  <h1>
@@ -7893,10 +8090,32 @@ body.dark .pinokio-custom-terminal-header {
7893
8090
  </button>
7894
8091
  <div class='sep'></div>
7895
8092
  <div class='app-header-identity' title="<%=config.title || name%>">
7896
- <% if (config.icon) { %>
7897
- <img class='app-header-identity-icon' src="<%=config.icon%>" alt="" aria-hidden="true"/>
7898
- <% } %>
7899
- <div class='app-header-identity-title'><%=config.title || name%></div>
8093
+ <div class="app-header-info" data-app-info-root>
8094
+ <button type="button" class="app-header-info-trigger" data-app-header-info-trigger aria-haspopup="dialog" aria-expanded="false" aria-label="App information">
8095
+ <% if (config.icon) { %>
8096
+ <img class='app-header-identity-icon' src="<%=config.icon%>" alt="" aria-hidden="true"/>
8097
+ <% } %>
8098
+ <span class='app-header-identity-title'><%=config.title || name%></span>
8099
+ </button>
8100
+ <div class="app-header-info-popover hidden" data-app-header-info-popover role="dialog" aria-label="App information">
8101
+ <div class="app-header-info-popover-head">Launcher</div>
8102
+ <div class="app-header-info-popover-main">
8103
+ <img class="app-header-info-popover-icon" src="<%=config.icon || '/pinokio-black.png'%>" alt="" aria-hidden="true"/>
8104
+ <div class="app-header-info-popover-copy">
8105
+ <div class="app-header-info-popover-title"><%=config.title || name%></div>
8106
+ <div class="app-header-info-popover-description"><%=config.description || 'No description provided.'%></div>
8107
+ </div>
8108
+ </div>
8109
+ <% if (appInfoRemoteUrl) { %>
8110
+ <a class="app-header-info-popover-link" href="<%=appInfoRemoteUrl%>" title="<%=appInfoRemoteUrl%>" rel="noopener noreferrer" data-app-info-external-link>
8111
+ <span><%=appInfoRemoteDisplayUrl%></span>
8112
+ <i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i>
8113
+ </a>
8114
+ <% } else { %>
8115
+ <div class="app-header-info-popover-link--muted">No browser-openable remote detected</div>
8116
+ <% } %>
8117
+ </div>
8118
+ </div>
7900
8119
  <span class='app-header-identity-separator' aria-hidden="true"></span>
7901
8120
  <div class="resource-usage" data-resource-usage-root data-workspace="<%=name%>">
7902
8121
  <button type="button" class="resource-usage-trigger" data-resource-usage-trigger aria-haspopup="dialog" aria-expanded="false" aria-label="Resource usage">
@@ -8419,10 +8638,32 @@ body.dark .pinokio-custom-terminal-header {
8419
8638
  <img class='mobile-bottom-nav__logo' src="/pinokio-black.png" alt="" aria-hidden="true">
8420
8639
  </button>
8421
8640
  <div class='mobile-bottom-nav__identity' title="<%=config.title || name%>" aria-label="<%=config.title || name%>">
8422
- <% if (config.icon) { %>
8423
- <img class='mobile-bottom-nav__identity-icon' src="<%=config.icon%>" alt="" aria-hidden="true"/>
8424
- <% } %>
8425
- <span class='mobile-bottom-nav__identity-title'><%=config.title || name%></span>
8641
+ <div class="app-header-info app-header-info--mobile" data-app-info-root>
8642
+ <button type="button" class="app-header-info-trigger" data-app-header-info-trigger aria-haspopup="dialog" aria-expanded="false" aria-label="App information">
8643
+ <% if (config.icon) { %>
8644
+ <img class='mobile-bottom-nav__identity-icon' src="<%=config.icon%>" alt="" aria-hidden="true"/>
8645
+ <% } %>
8646
+ <span class='mobile-bottom-nav__identity-title'><%=config.title || name%></span>
8647
+ </button>
8648
+ <div class="app-header-info-popover hidden" data-app-header-info-popover role="dialog" aria-label="App information">
8649
+ <div class="app-header-info-popover-head">Launcher</div>
8650
+ <div class="app-header-info-popover-main">
8651
+ <img class="app-header-info-popover-icon" src="<%=config.icon || '/pinokio-black.png'%>" alt="" aria-hidden="true"/>
8652
+ <div class="app-header-info-popover-copy">
8653
+ <div class="app-header-info-popover-title"><%=config.title || name%></div>
8654
+ <div class="app-header-info-popover-description"><%=config.description || 'No description provided.'%></div>
8655
+ </div>
8656
+ </div>
8657
+ <% if (appInfoRemoteUrl) { %>
8658
+ <a class="app-header-info-popover-link" href="<%=appInfoRemoteUrl%>" title="<%=appInfoRemoteUrl%>" rel="noopener noreferrer" data-app-info-external-link>
8659
+ <span><%=appInfoRemoteDisplayUrl%></span>
8660
+ <i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i>
8661
+ </a>
8662
+ <% } else { %>
8663
+ <div class="app-header-info-popover-link--muted">No browser-openable remote detected</div>
8664
+ <% } %>
8665
+ </div>
8666
+ </div>
8426
8667
  <span class='mobile-bottom-nav__identity-separator' aria-hidden="true"></span>
8427
8668
  <div class="resource-usage" data-resource-usage-root data-resource-always-visible="true" data-workspace="<%=name%>">
8428
8669
  <button type="button" class="resource-usage-trigger" data-resource-usage-trigger aria-haspopup="dialog" aria-expanded="false" aria-label="Resource usage">
@@ -16152,6 +16393,78 @@ const rerenderMenuSection = (container, html) => {
16152
16393
  })()
16153
16394
  </script>
16154
16395
  <script>
16396
+ (() => {
16397
+ const appInfoRoots = Array.from(document.querySelectorAll("[data-app-info-root]"))
16398
+ if (appInfoRoots.length === 0) {
16399
+ return
16400
+ }
16401
+
16402
+ const closeAppInfoPopovers = (exceptRoot = null) => {
16403
+ appInfoRoots.forEach((root) => {
16404
+ if (root === exceptRoot) return
16405
+ const trigger = root.querySelector("[data-app-header-info-trigger]")
16406
+ const popover = root.querySelector("[data-app-header-info-popover]")
16407
+ if (trigger) trigger.setAttribute("aria-expanded", "false")
16408
+ if (popover) popover.classList.add("hidden")
16409
+ })
16410
+ }
16411
+
16412
+ appInfoRoots.forEach((root) => {
16413
+ const trigger = root.querySelector("[data-app-header-info-trigger]")
16414
+ const popover = root.querySelector("[data-app-header-info-popover]")
16415
+ if (!trigger || !popover) {
16416
+ return
16417
+ }
16418
+ trigger.addEventListener("click", (event) => {
16419
+ event.preventDefault()
16420
+ event.stopPropagation()
16421
+ const isOpen = !popover.classList.contains("hidden")
16422
+ closeAppInfoPopovers(root)
16423
+ popover.classList.toggle("hidden", isOpen)
16424
+ trigger.setAttribute("aria-expanded", isOpen ? "false" : "true")
16425
+ })
16426
+ root.querySelectorAll("[data-app-info-external-link]").forEach((link) => {
16427
+ link.addEventListener("click", (event) => {
16428
+ const href = link.getAttribute("href") || ""
16429
+ if (!href) {
16430
+ return
16431
+ }
16432
+ event.preventDefault()
16433
+ event.stopPropagation()
16434
+ const agent = document.body.getAttribute("data-agent")
16435
+ if (agent === "electron") {
16436
+ window.open(href, "_blank", "browser")
16437
+ } else {
16438
+ window.open(href, "_blank")
16439
+ }
16440
+ closeAppInfoPopovers()
16441
+ })
16442
+ })
16443
+ })
16444
+
16445
+ document.addEventListener("pointerdown", (event) => {
16446
+ const target = event.target
16447
+ if (target && target.closest && target.closest("[data-app-info-root]")) {
16448
+ return
16449
+ }
16450
+ closeAppInfoPopovers()
16451
+ }, true)
16452
+ window.addEventListener("blur", () => {
16453
+ window.setTimeout(() => {
16454
+ const activeElement = document.activeElement
16455
+ if (activeElement && activeElement.tagName === "IFRAME") {
16456
+ closeAppInfoPopovers()
16457
+ }
16458
+ }, 0)
16459
+ })
16460
+ document.addEventListener("keydown", (event) => {
16461
+ if (event.key === "Escape") {
16462
+ closeAppInfoPopovers()
16463
+ }
16464
+ })
16465
+ })()
16466
+ </script>
16467
+ <script>
16155
16468
  (() => {
16156
16469
  const appcanvas = document.querySelector(".appcanvas")
16157
16470
  const aside = document.getElementById("app-sidebar")
@@ -537,6 +537,128 @@ test("app sidebar auto-peeks on collapsed idle run page and dismisses after clic
537
537
  assert.equal(activeDom.window.localStorage.getItem(storageKey), "1")
538
538
  })
539
539
 
540
+ test("app identity popover exposes launcher metadata and remote browser link", async () => {
541
+ const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
542
+ const server = await fs.readFile(path.resolve(root, "server/index.js"), "utf8")
543
+ const chromeStart = server.indexOf(" async chrome(req, res, type, options) {")
544
+ const existingCommunityGitConfigIndex = server.indexOf("const gitRemote = await git.getConfig", chromeStart)
545
+ const helperIndex = server.indexOf("const buildGitRemoteWebUrl = (value) =>", chromeStart)
546
+ const launcherRemoteIndex = server.indexOf("const launcherRemote =", chromeStart)
547
+ const renderPropIndex = server.indexOf("launcher_remote_url: launcherRemoteUrl", launcherRemoteIndex)
548
+
549
+ assert.ok(chromeStart >= 0)
550
+ assert.ok(existingCommunityGitConfigIndex > chromeStart)
551
+ assert.ok(helperIndex > existingCommunityGitConfigIndex)
552
+ assert.ok(helperIndex < launcherRemoteIndex)
553
+ assert.ok(renderPropIndex > launcherRemoteIndex)
554
+ assert.match(server, /kernel\.api\.parentGitURI\(this\.kernel\.path\("api", name\)\)/)
555
+ assert.match(server, /launcherRemoteUrl = buildGitRemoteWebUrl\(launcherRemote\)/)
556
+ assert.match(server, /launcher_remote_url: launcherRemoteUrl/)
557
+ assert.match(server, /const buildGitRemoteWebUrl = \(value\) =>/)
558
+ assert.match(server, /return `https:\/\/\$\{sshUrlMatch\[1\]\}\/\$\{sshUrlMatch\[2\]\}`/)
559
+ assert.match(server, /return `https:\/\/\$\{scpMatch\[1\]\}\/\$\{scpMatch\[2\]\}`/)
560
+ assert.match(appView, /data-app-info-root/)
561
+ assert.match(appView, /data-app-header-info-trigger/)
562
+ assert.match(appView, /data-app-header-info-popover/)
563
+ assert.match(appView, /width: min\(390px, calc\(100vw - 24px\)\);/)
564
+ assert.match(appView, /class="app-header-info-popover-icon" src="<%=config\.icon \|\| '\/pinokio-black\.png'%>"/)
565
+ assert.match(appView, /const appInfoRemoteUrl = typeof launcher_remote_url === "string" \? launcher_remote_url : ""/)
566
+ assert.ok(appView.includes('const appInfoRemoteDisplayUrl = appInfoRemoteUrl.replace(/^https?:\\/\\//i, "")'))
567
+ assert.match(appView, /href="<%=appInfoRemoteUrl%>" title="<%=appInfoRemoteUrl%>" rel="noopener noreferrer" data-app-info-external-link/)
568
+ assert.match(appView, /<span><%=appInfoRemoteDisplayUrl%><\/span>/)
569
+ assert.match(appView, /text-overflow: ellipsis/)
570
+ assert.match(appView, /white-space: nowrap/)
571
+ assert.match(appView, /window\.open\(href, "_blank"\)/)
572
+ assert.match(appView, /window\.open\(href, "_blank", "browser"\)/)
573
+ assert.match(appView, /No browser-openable remote detected/)
574
+ assert.match(appView, /\.mobile-bottom-nav__identity \{[\s\S]*overflow: visible;/)
575
+ assert.match(appView, /\.mobile-bottom-nav \.app-header-info-popover \{[\s\S]*bottom: calc\(100% \+ 9px\);[\s\S]*width: min\(390px, calc\(100vw - 24px\)\);/)
576
+ assert.match(appView, /\.mobile-bottom-nav \.resource-usage-popover \{[\s\S]*bottom: calc\(100% \+ 9px\);/)
577
+ assert.doesNotMatch(appView, /Open launcher remote/)
578
+ assert.doesNotMatch(appView, /fetch\("\/go"/)
579
+ assert.doesNotMatch(appView, /app-header-info-popover-link"[^>]*features="browser"/)
580
+ assert.doesNotMatch(appView, /appInfoGitHistoryUrl|hydrateAppInfoGithubUrl|buildAppInfoGithubUrl|launcher_github_url|Open launcher on GitHub|No GitHub remote detected/)
581
+
582
+ const start = appView.indexOf('<script>\n(() => {\n const appInfoRoots = Array.from(document.querySelectorAll("[data-app-info-root]"))')
583
+ const end = appView.indexOf("</script>", start)
584
+ assert.notEqual(start, -1)
585
+ assert.notEqual(end, -1)
586
+ const appInfoScript = appView.slice(start + "<script>\n".length, end)
587
+ const dom = new JSDOM(`<!doctype html>
588
+ <body>
589
+ <div data-app-info-root>
590
+ <button type="button" data-app-header-info-trigger aria-expanded="false">Demo</button>
591
+ <div class="hidden" data-app-header-info-popover>
592
+ <a class="app-header-info-popover-link" href="https://gitlab.com/octocat/demo" title="https://gitlab.com/octocat/demo" data-app-info-external-link>gitlab.com/octocat/demo</a>
593
+ </div>
594
+ </div>
595
+ <iframe></iframe>
596
+ </body>`, {
597
+ url: "http://127.0.0.1:42000/v/demo",
598
+ runScripts: "outside-only",
599
+ pretendToBeVisual: true
600
+ })
601
+ const openedUrls = []
602
+ dom.window.open = (url, target, features) => {
603
+ openedUrls.push({ url, target, features })
604
+ }
605
+
606
+ dom.window.eval(appInfoScript)
607
+ const trigger = dom.window.document.querySelector("[data-app-header-info-trigger]")
608
+ const popover = dom.window.document.querySelector("[data-app-header-info-popover]")
609
+ const click = () => trigger.dispatchEvent(new dom.window.MouseEvent("click", { bubbles: true, cancelable: true }))
610
+
611
+ assert.equal(popover.classList.contains("hidden"), true)
612
+ assert.equal(trigger.getAttribute("aria-expanded"), "false")
613
+ const remoteLink = dom.window.document.querySelector(".app-header-info-popover-link")
614
+ assert.ok(remoteLink)
615
+ assert.equal(remoteLink.getAttribute("href"), "https://gitlab.com/octocat/demo")
616
+ assert.equal(remoteLink.getAttribute("title"), "https://gitlab.com/octocat/demo")
617
+ assert.equal(remoteLink.hasAttribute("target"), false)
618
+ assert.equal(remoteLink.hasAttribute("features"), false)
619
+ assert.equal(remoteLink.textContent.trim(), "gitlab.com/octocat/demo")
620
+
621
+ click()
622
+ assert.equal(popover.classList.contains("hidden"), false)
623
+ assert.equal(trigger.getAttribute("aria-expanded"), "true")
624
+ remoteLink.dispatchEvent(new dom.window.MouseEvent("click", { bubbles: true, cancelable: true }))
625
+ assert.equal(openedUrls.length, 1)
626
+ assert.deepEqual(openedUrls[0], {
627
+ url: "https://gitlab.com/octocat/demo",
628
+ target: "_blank",
629
+ features: undefined
630
+ })
631
+ assert.equal(popover.classList.contains("hidden"), true)
632
+ assert.equal(trigger.getAttribute("aria-expanded"), "false")
633
+
634
+ click()
635
+ assert.equal(popover.classList.contains("hidden"), false)
636
+ assert.equal(trigger.getAttribute("aria-expanded"), "true")
637
+
638
+ click()
639
+ assert.equal(popover.classList.contains("hidden"), true)
640
+ assert.equal(trigger.getAttribute("aria-expanded"), "false")
641
+
642
+ click()
643
+ dom.window.document.body.dispatchEvent(new dom.window.Event("pointerdown", { bubbles: true }))
644
+ assert.equal(popover.classList.contains("hidden"), true)
645
+ assert.equal(trigger.getAttribute("aria-expanded"), "false")
646
+
647
+ click()
648
+ dom.window.document.dispatchEvent(new dom.window.KeyboardEvent("keydown", { key: "Escape", bubbles: true }))
649
+ assert.equal(popover.classList.contains("hidden"), true)
650
+ assert.equal(trigger.getAttribute("aria-expanded"), "false")
651
+
652
+ dom.window.document.body.setAttribute("data-agent", "electron")
653
+ click()
654
+ remoteLink.dispatchEvent(new dom.window.MouseEvent("click", { bubbles: true, cancelable: true }))
655
+ assert.deepEqual(openedUrls[1], {
656
+ url: "https://gitlab.com/octocat/demo",
657
+ target: "_blank",
658
+ features: "browser"
659
+ })
660
+ })
661
+
540
662
  test("static guard: home run command selection opens without launching before selecting command", async () => {
541
663
  const homeRunMenu = await fs.readFile(path.resolve(root, "server/views/partials/home_run_menu.ejs"), "utf8")
542
664