pinokiod 7.3.14 → 7.4.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.
Files changed (51) hide show
  1. package/kernel/api/index.js +111 -15
  2. package/kernel/api/script/index.js +10 -0
  3. package/kernel/autolaunch.js +111 -0
  4. package/kernel/environment.js +89 -1
  5. package/kernel/git.js +9 -19
  6. package/kernel/index.js +142 -43
  7. package/kernel/launch_requirements.js +1115 -0
  8. package/kernel/ready.js +231 -0
  9. package/kernel/script.js +16 -0
  10. package/kernel/shells.js +9 -1
  11. package/kernel/workspace_status.js +111 -45
  12. package/package.json +1 -1
  13. package/server/autolaunch.js +725 -0
  14. package/server/index.js +244 -411
  15. package/server/public/logs.js +15 -1
  16. package/server/public/style.css +99 -31
  17. package/server/views/app.ejs +263 -159
  18. package/server/views/autolaunch.ejs +363 -75
  19. package/server/views/index.ejs +550 -26
  20. package/server/views/logs.ejs +14 -12
  21. package/server/views/partials/app_autolaunch_dependency_events.ejs +99 -0
  22. package/server/views/partials/app_autolaunch_dependency_save.ejs +10 -0
  23. package/server/views/partials/app_autolaunch_dependency_styles.ejs +357 -0
  24. package/server/views/partials/app_autolaunch_modal_helpers.ejs +347 -0
  25. package/server/views/partials/autolaunch_dependency_helpers.ejs +204 -0
  26. package/server/views/partials/autolaunch_dependency_save.ejs +13 -0
  27. package/server/views/partials/autolaunch_dependency_styles.ejs +347 -0
  28. package/server/views/partials/home_action_modal.ejs +4 -1
  29. package/server/views/partials/launch_requirements_status_client.ejs +271 -0
  30. package/server/views/partials/launch_requirements_status_styles.ejs +171 -0
  31. package/server/views/partials/launch_settings_dependency_save_factory.ejs +45 -0
  32. package/server/views/partials/launch_settings_dependency_script_loader_factory.ejs +25 -0
  33. package/server/views/terminal.ejs +196 -2
  34. package/test/home-autolaunch-live-ui.test.js +455 -0
  35. package/test/launch-requirements-browser.test.js +579 -0
  36. package/test/launch-requirements-contract-coverage.test.js +627 -0
  37. package/test/launch-requirements-real-browser.js +625 -0
  38. package/test/launch-requirements-status-client.test.js +132 -0
  39. package/test/launch-requirements.test.js +1806 -0
  40. package/test/launch-settings-ui.test.js +370 -0
  41. package/test/ready-state.test.js +49 -0
  42. package/test/server-autolaunch.test.js +1052 -0
  43. package/test/startup-git-index-benchmark.js +409 -0
  44. package/test/startup-git-index-browser.js +320 -0
  45. package/test/startup-git-index-performance.test.js +380 -0
  46. package/test/startup-git-index-refactor.test.js +450 -0
  47. package/test/startup-git-index-route.test.js +588 -0
  48. package/test/universal-launcher.smoke.spec.js +10 -9
  49. package/test/workspace-gitignore-benchmark.js +815 -0
  50. package/test/workspace-gitignore-path-scoped.test.js +256 -0
  51. package/spec/INSTRUCTION_SYNC.md +0 -432
@@ -0,0 +1,579 @@
1
+ const assert = require("node:assert/strict")
2
+ const fsSync = require("node:fs")
3
+ const fs = require("node:fs/promises")
4
+ const http = require("node:http")
5
+ const path = require("node:path")
6
+ const childProcess = require("node:child_process")
7
+ const test = require("node:test")
8
+ const ejs = require("ejs")
9
+
10
+ function loadPlaywright() {
11
+ try {
12
+ return require("playwright")
13
+ } catch (_) {}
14
+
15
+ try {
16
+ const bin = childProcess.execFileSync("which", ["playwright"], { encoding: "utf8" }).trim()
17
+ if (!bin) {
18
+ return null
19
+ }
20
+ return require(path.join(path.dirname(path.dirname(bin)), "playwright"))
21
+ } catch (_) {
22
+ return null
23
+ }
24
+ }
25
+
26
+ const playwright = loadPlaywright()
27
+
28
+ const root = path.resolve(__dirname, "..")
29
+ const appViewPath = path.resolve(root, "server/views/app.ejs")
30
+ const statusClientPath = path.resolve(root, "server/views/partials/launch_requirements_status_client.ejs")
31
+ const homeViewPath = path.resolve(root, "server/views/index.ejs")
32
+
33
+ const browserTest = test
34
+
35
+ function chromiumExecutablePath() {
36
+ const candidates = [
37
+ process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE,
38
+ playwright && playwright.chromium.executablePath(),
39
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
40
+ "/Applications/Chromium.app/Contents/MacOS/Chromium"
41
+ ].filter(Boolean)
42
+ return candidates.find((candidate) => fsSync.existsSync(candidate)) || ""
43
+ }
44
+
45
+ async function renderAppAutolaunchScript(initialState) {
46
+ const view = await fs.readFile(appViewPath, "utf8")
47
+ const start = view.indexOf(';(function() {\n const root = document.querySelector("[data-app-autolaunch]")')
48
+ assert.notEqual(start, -1)
49
+ const end = view.indexOf('\n\n;(function() {\n const scroller = document.querySelector(".appcanvas > aside .menu-scroller")', start)
50
+ assert.notEqual(end, -1)
51
+ return ejs.render(view.slice(start, end), {
52
+ autolaunch_app: initialState
53
+ }, {
54
+ filename: appViewPath
55
+ })
56
+ }
57
+
58
+ async function renderLaunchRequirementsStatusClient(enabled) {
59
+ const template = await fs.readFile(statusClientPath, "utf8")
60
+ return ejs.render(template, {
61
+ name: "target",
62
+ config: { title: "Target" },
63
+ launch_requirements_status_enabled: !!enabled
64
+ }, {
65
+ filename: statusClientPath
66
+ })
67
+ }
68
+
69
+ async function renderHomeAutolaunchScript() {
70
+ const view = await fs.readFile(homeViewPath, "utf8")
71
+ const start = view.indexOf("const startHomeAutolaunchPolling = () => {")
72
+ assert.notEqual(start, -1)
73
+ const call = view.indexOf("startHomeAutolaunchPolling()", start)
74
+ assert.notEqual(call, -1)
75
+ return ejs.render(view.slice(start, call + "startHomeAutolaunchPolling()".length), {
76
+ launch_complete: false
77
+ }, {
78
+ filename: homeViewPath
79
+ })
80
+ }
81
+
82
+ function html(body, script = "") {
83
+ return `<!doctype html>
84
+ <html>
85
+ <head>
86
+ <meta charset="utf-8">
87
+ <style>
88
+ .hidden, [hidden] { display: none !important; }
89
+ body { font-family: sans-serif; }
90
+ .app-autolaunch-modal { position: fixed; inset: 20px; background: white; border: 1px solid #ccc; padding: 16px; }
91
+ .app-autolaunch-script-option, .app-autolaunch-dependency-option, .app-autolaunch-dependency-row { display: block; margin: 6px 0; }
92
+ .launch-requirements-card { width: 560px; margin: 40px auto; }
93
+ .launch-requirements-row { display: grid; grid-template-columns: 32px 1fr auto; gap: 8px; align-items: center; }
94
+ </style>
95
+ </head>
96
+ <body>
97
+ ${body}
98
+ <script>${script}</script>
99
+ </body>
100
+ </html>`
101
+ }
102
+
103
+ async function appFixtureHtml(initialApp) {
104
+ const script = await renderAppAutolaunchScript(initialApp)
105
+ return html(`
106
+ <div class="app-autolaunch" data-app-autolaunch data-app-id="target">
107
+ <button type="button" class="app-autolaunch-row" data-app-autolaunch-button data-enabled="${initialApp.autolaunch_enabled ? "true" : "false"}" aria-haspopup="dialog" aria-expanded="false">
108
+ <span class="app-autolaunch-label">Autolaunch</span>
109
+ <span class="app-autolaunch-status" data-app-autolaunch-status>${initialApp.autolaunch_enabled ? "ON" : "OFF"}</span>
110
+ </button>
111
+ <div class="app-autolaunch-modal hidden" data-app-autolaunch-modal role="dialog" aria-modal="true" aria-label="Autolaunch">
112
+ <button type="button" class="app-autolaunch-switch" role="switch" aria-checked="${initialApp.autolaunch_enabled ? "true" : "false"}" data-app-autolaunch-switch aria-label="Start with Pinokio">
113
+ <span data-app-autolaunch-switch-label>${initialApp.autolaunch_enabled ? "ON" : "OFF"}</span>
114
+ </button>
115
+ <button type="button" data-app-autolaunch-close>Close</button>
116
+ <section>
117
+ <div data-app-autolaunch-scripts></div>
118
+ </section>
119
+ <section>
120
+ <div data-app-autolaunch-dependencies></div>
121
+ </section>
122
+ <div data-app-autolaunch-feedback></div>
123
+ </div>
124
+ </div>
125
+ `, script)
126
+ }
127
+
128
+ async function openWithoutLaunchingHtml() {
129
+ const script = await renderLaunchRequirementsStatusClient(false)
130
+ return html(`
131
+ <main>
132
+ <div data-launch-requirements-status hidden></div>
133
+ <div data-open-without-launching-content>Opened without launching.</div>
134
+ </main>
135
+ `, `
136
+ window.Socket = function Socket() {
137
+ throw new Error("Socket must not start for open without launching")
138
+ };
139
+ ${script}
140
+ `)
141
+ }
142
+
143
+ async function statusFixtureHtml() {
144
+ const script = await renderLaunchRequirementsStatusClient(true)
145
+ return html(`
146
+ <main>
147
+ <div data-launch-requirements-status hidden></div>
148
+ </main>
149
+ `, `
150
+ window.Socket = function Socket() {
151
+ this.run = function() { return Promise.resolve() }
152
+ this.close = function() {}
153
+ };
154
+ ${script}
155
+ `)
156
+ }
157
+
158
+ async function homeFixtureHtml() {
159
+ const script = await renderHomeAutolaunchScript()
160
+ return html(`
161
+ <script>
162
+ window.reorderHomeSectionsByPreference = function() {};
163
+ </script>
164
+ <div class="running-apps"></div>
165
+ <div class="not-running-apps">
166
+ <div class="line align-top home-app-line" data-autolaunch-app="target" data-autolaunch-starting="0" data-autolaunch-script="start.js">
167
+ <h3><span class="title"><i class="fa-solid fa-circle"></i></span><span>Target</span></h3>
168
+ <div class="menu-btns">
169
+ <button class="open-actions-modal" data-dialog-id="actions-target" type="button">menu</button>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ <div id="actions-target"><div class="home-actions-title-row"></div></div>
174
+ `, script)
175
+ }
176
+
177
+ function createJsonResponse(res, status, payload) {
178
+ res.writeHead(status, {
179
+ "Content-Type": "application/json",
180
+ "Cache-Control": "no-store"
181
+ })
182
+ res.end(JSON.stringify(payload))
183
+ }
184
+
185
+ function readBody(req) {
186
+ return new Promise((resolve, reject) => {
187
+ let body = ""
188
+ req.setEncoding("utf8")
189
+ req.on("data", (chunk) => { body += chunk })
190
+ req.on("end", () => {
191
+ try {
192
+ resolve(body ? JSON.parse(body) : {})
193
+ } catch (error) {
194
+ reject(error)
195
+ }
196
+ })
197
+ req.on("error", reject)
198
+ })
199
+ }
200
+
201
+ async function startFixtureServer(state) {
202
+ const pages = {
203
+ app: await appFixtureHtml(state.targetApp),
204
+ openWithoutLaunching: await openWithoutLaunchingHtml(),
205
+ status: await statusFixtureHtml(),
206
+ home: await homeFixtureHtml()
207
+ }
208
+ const server = http.createServer(async (req, res) => {
209
+ try {
210
+ const url = new URL(req.url, "http://127.0.0.1")
211
+ if (req.method === "GET" && url.pathname === "/app-fixture") {
212
+ res.writeHead(200, { "Content-Type": "text/html" })
213
+ res.end(pages.app)
214
+ return
215
+ }
216
+ if (req.method === "GET" && url.pathname === "/open-without-launching") {
217
+ res.writeHead(200, { "Content-Type": "text/html" })
218
+ res.end(pages.openWithoutLaunching)
219
+ return
220
+ }
221
+ if (req.method === "GET" && url.pathname === "/status-fixture") {
222
+ res.writeHead(200, { "Content-Type": "text/html" })
223
+ res.end(pages.status)
224
+ return
225
+ }
226
+ if (req.method === "GET" && url.pathname === "/home-fixture") {
227
+ res.writeHead(200, { "Content-Type": "text/html" })
228
+ res.end(pages.home)
229
+ return
230
+ }
231
+ if (req.method === "GET" && url.pathname === "/autolaunch/candidates") {
232
+ const appId = url.searchParams.get("app")
233
+ state.candidateRequests.push(appId)
234
+ const payload = state.candidates[appId]
235
+ createJsonResponse(res, payload ? 200 : 404, payload || { ok: false, error: "missing app" })
236
+ return
237
+ }
238
+ if (req.method === "POST" && url.pathname === "/autolaunch") {
239
+ const body = await readBody(req)
240
+ state.autolaunchPosts.push(body)
241
+ const app = {
242
+ id: body.app,
243
+ title: body.app === "helper" ? "Helper" : "Target",
244
+ autolaunch: body.clear_script ? "" : (body.script || ""),
245
+ autolaunch_enabled: !!body.enabled,
246
+ autolaunch_depends: body.app === "target" ? state.targetDependencies.slice() : []
247
+ }
248
+ if (state.candidates.target && Array.isArray(state.candidates.target.dependency_apps)) {
249
+ state.candidates.target.dependency_apps = state.candidates.target.dependency_apps.map((candidate) => {
250
+ return candidate.id === app.id ? Object.assign({}, candidate, app) : candidate
251
+ })
252
+ }
253
+ if (body.app === "target") {
254
+ state.targetApp = Object.assign({}, state.targetApp, app)
255
+ if (state.candidates.target) {
256
+ state.candidates.target.current = app.autolaunch
257
+ state.candidates.target.app = Object.assign({}, state.candidates.target.app, app)
258
+ }
259
+ }
260
+ createJsonResponse(res, 200, { ok: true, app })
261
+ return
262
+ }
263
+ if (req.method === "POST" && url.pathname === "/autolaunch/dependencies") {
264
+ const body = await readBody(req)
265
+ state.dependencyPosts.push(body)
266
+ state.targetDependencies = Array.isArray(body.dependencies) ? body.dependencies.slice() : []
267
+ const app = Object.assign({}, state.targetApp, {
268
+ autolaunch_depends: state.targetDependencies.slice()
269
+ })
270
+ createJsonResponse(res, 200, { ok: true, app })
271
+ return
272
+ }
273
+ if (req.method === "GET" && url.pathname.startsWith("/pinokio/launch-requirements/")) {
274
+ state.launchRequirementsGets += 1
275
+ createJsonResponse(res, 200, {
276
+ ok: true,
277
+ status: state.launchRequirementsStatus || {
278
+ state: "waiting",
279
+ requirements: [{ id: "helper", title: "Helper", state: "starting", script: "start.js" }]
280
+ }
281
+ })
282
+ return
283
+ }
284
+ if (req.method === "GET" && url.pathname === "/pinokio/home_status") {
285
+ state.homeStatusGets += 1
286
+ createJsonResponse(res, 200, state.homeStatus)
287
+ return
288
+ }
289
+ res.writeHead(404, { "Content-Type": "text/plain" })
290
+ res.end("not found")
291
+ } catch (error) {
292
+ res.writeHead(500, { "Content-Type": "text/plain" })
293
+ res.end(error && error.stack ? error.stack : String(error))
294
+ }
295
+ })
296
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve))
297
+ return {
298
+ server,
299
+ baseUrl: `http://127.0.0.1:${server.address().port}`
300
+ }
301
+ }
302
+
303
+ function baseState(options = {}) {
304
+ const targetScript = options.targetScript || ""
305
+ const targetApp = {
306
+ id: "target",
307
+ title: "Target",
308
+ autolaunch: targetScript,
309
+ autolaunch_enabled: false,
310
+ autolaunch_depends: []
311
+ }
312
+ const helperScript = Object.prototype.hasOwnProperty.call(options, "helperScript")
313
+ ? options.helperScript
314
+ : "start.js"
315
+ return {
316
+ targetApp,
317
+ targetDependencies: [],
318
+ candidateRequests: [],
319
+ autolaunchPosts: [],
320
+ dependencyPosts: [],
321
+ launchRequirementsGets: 0,
322
+ homeStatusGets: 0,
323
+ homeStatus: {
324
+ launch_complete: true,
325
+ running_apps: [],
326
+ running_scripts: [],
327
+ autolaunch: { apps: {} }
328
+ },
329
+ candidates: {
330
+ target: {
331
+ ok: true,
332
+ current: targetScript,
333
+ app: Object.assign({}, targetApp),
334
+ menu: [{ script: "start.js", label: "Start", menu_default: true }],
335
+ other: [{ script: "target.custom.js", label: "Custom" }],
336
+ dependency_apps: [{
337
+ id: "helper",
338
+ title: "Helper",
339
+ workspace_path: "/Users/test/pinokio/api/helper",
340
+ icon: "/helper.png",
341
+ autolaunch: helperScript,
342
+ autolaunch_enabled: false
343
+ }]
344
+ },
345
+ helper: {
346
+ ok: true,
347
+ current: helperScript,
348
+ app: {
349
+ id: "helper",
350
+ title: "Helper",
351
+ autolaunch: helperScript,
352
+ autolaunch_enabled: false,
353
+ autolaunch_depends: []
354
+ },
355
+ menu: [{ script: "start.js", label: "Start", menu_default: true }],
356
+ other: [{ script: "helper.custom.js", label: "Custom" }],
357
+ dependency_apps: []
358
+ }
359
+ }
360
+ }
361
+ }
362
+
363
+ async function withBrowser(state, callback) {
364
+ assert.ok(playwright, "Playwright is required. Run: npx -y -p playwright node --test test/launch-requirements-browser.test.js")
365
+ const { server, baseUrl } = await startFixtureServer(state)
366
+ let browser = null
367
+ let page = null
368
+ const errors = []
369
+ try {
370
+ const executablePath = chromiumExecutablePath()
371
+ assert.ok(executablePath, "No Chromium executable is available for browser tests")
372
+ browser = await playwright.chromium.launch({
373
+ headless: true,
374
+ executablePath
375
+ })
376
+ page = await browser.newPage()
377
+ page.on("pageerror", (error) => errors.push(error.message || String(error)))
378
+ await callback({ page, baseUrl, state })
379
+ assert.deepEqual(errors, [])
380
+ } finally {
381
+ if (page) {
382
+ await page.close().catch(() => {})
383
+ }
384
+ if (browser) {
385
+ await browser.close().catch(() => {})
386
+ }
387
+ await new Promise((resolve) => server.close(resolve))
388
+ }
389
+ }
390
+
391
+ async function textContent(page, selector) {
392
+ return page.locator(selector).first().textContent()
393
+ }
394
+
395
+ browserTest("browser: selecting a launch script from empty state persists and stays off", async () => {
396
+ const state = baseState({ targetScript: "" })
397
+ await withBrowser(state, async ({ page, baseUrl }) => {
398
+ await page.goto(`${baseUrl}/app-fixture`)
399
+ await page.click("[data-app-autolaunch-button]")
400
+ await page.waitForSelector('input[name="app-autolaunch-script"][value="start.js"]')
401
+ await page.click('input[name="app-autolaunch-script"][value="start.js"]')
402
+ await page.waitForFunction(() => {
403
+ const feedback = document.querySelector("[data-app-autolaunch-feedback]")
404
+ return feedback && feedback.textContent.includes("Launch script saved.")
405
+ })
406
+
407
+ assert.deepEqual(state.autolaunchPosts[0], {
408
+ app: "target",
409
+ script: "start.js",
410
+ enabled: false
411
+ })
412
+ assert.equal(await page.isChecked('input[name="app-autolaunch-script"][value="start.js"]'), true)
413
+ assert.equal((await textContent(page, "[data-app-autolaunch-status]")).trim(), "OFF")
414
+ })
415
+ })
416
+
417
+ browserTest("browser: adding a requirement with an existing script still requires script confirmation", async () => {
418
+ const state = baseState({ targetScript: "target.custom.js", helperScript: "start.js" })
419
+ await withBrowser(state, async ({ page, baseUrl }) => {
420
+ await page.goto(`${baseUrl}/app-fixture`)
421
+ await page.click("[data-app-autolaunch-button]")
422
+ await page.waitForSelector("[data-app-autolaunch-toggle-dependency-picker]")
423
+ await page.click("[data-app-autolaunch-toggle-dependency-picker]")
424
+ await page.click('[data-app-autolaunch-add-dependency="helper"]')
425
+
426
+ await page.waitForSelector(".app-autolaunch-dependency-script-modal")
427
+ assert.equal(state.dependencyPosts.length, 0)
428
+ assert.equal(await page.isChecked('[data-app-autolaunch-dependency-script-app="helper"][value="start.js"]'), true)
429
+
430
+ await page.click('[data-app-autolaunch-dependency-script-app="helper"][value="start.js"]')
431
+ assert.equal(state.dependencyPosts.length, 0)
432
+ assert.equal(state.autolaunchPosts.length, 0)
433
+ await page.click('[data-app-autolaunch-confirm-dependency-script]')
434
+ await page.waitForFunction(() => {
435
+ const feedback = document.querySelector("[data-app-autolaunch-feedback]")
436
+ return feedback && feedback.textContent.includes("Requirement saved.")
437
+ })
438
+
439
+ assert.deepEqual(state.autolaunchPosts.at(-1), {
440
+ app: "helper",
441
+ script: "start.js",
442
+ enabled: false
443
+ })
444
+ assert.deepEqual(state.dependencyPosts.at(-1), {
445
+ app: "target",
446
+ dependencies: ["helper"]
447
+ })
448
+ assert.equal(await page.locator(".app-autolaunch-dependency-script-modal").count(), 0)
449
+ })
450
+ })
451
+
452
+ browserTest("browser: blocked setup status exposes script selection and stop actions", async () => {
453
+ const state = baseState({ targetScript: "target.custom.js", helperScript: "" })
454
+ state.launchRequirementsStatus = {
455
+ state: "blocked",
456
+ blocked_reason: "Comfyui has no launch script selected",
457
+ requirements: [{
458
+ id: "helper",
459
+ title: "Comfyui",
460
+ state: "blocked",
461
+ blocked_reason: "Comfyui has no launch script selected",
462
+ icon: "/pinokio-black.png"
463
+ }]
464
+ }
465
+ await withBrowser(state, async ({ page, baseUrl }) => {
466
+ await page.goto(`${baseUrl}/status-fixture`)
467
+ await page.waitForSelector("text=Launch needs setup")
468
+ assert.equal(await page.locator('[data-launch-requirements-choose-script="helper"]').count(), 1)
469
+ assert.equal(await page.locator("[data-launch-requirements-stop]").count(), 1)
470
+ })
471
+ })
472
+
473
+ browserTest("browser: adding a requirement with no script requires selecting one before saving", async () => {
474
+ const state = baseState({ targetScript: "target.custom.js", helperScript: "" })
475
+ await withBrowser(state, async ({ page, baseUrl }) => {
476
+ await page.goto(`${baseUrl}/app-fixture`)
477
+ await page.click("[data-app-autolaunch-button]")
478
+ await page.waitForSelector("[data-app-autolaunch-toggle-dependency-picker]")
479
+ await page.click("[data-app-autolaunch-toggle-dependency-picker]")
480
+ await page.click('[data-app-autolaunch-add-dependency="helper"]')
481
+
482
+ await page.waitForSelector(".app-autolaunch-dependency-script-modal")
483
+ assert.equal(state.dependencyPosts.length, 0)
484
+ assert.equal(await page.isChecked('[data-app-autolaunch-dependency-script-app="helper"][value="start.js"]'), false)
485
+
486
+ await page.click('[data-app-autolaunch-dependency-script-app="helper"][value="start.js"]')
487
+ assert.equal(state.dependencyPosts.length, 0)
488
+ assert.equal(state.autolaunchPosts.length, 0)
489
+ await page.click('[data-app-autolaunch-confirm-dependency-script]')
490
+ await page.waitForFunction(() => {
491
+ const feedback = document.querySelector("[data-app-autolaunch-feedback]")
492
+ return feedback && feedback.textContent.includes("Requirement saved.")
493
+ })
494
+
495
+ assert.deepEqual(state.autolaunchPosts.at(-1), {
496
+ app: "helper",
497
+ script: "start.js",
498
+ enabled: false
499
+ })
500
+ assert.deepEqual(state.dependencyPosts.at(-1), {
501
+ app: "target",
502
+ dependencies: ["helper"]
503
+ })
504
+ })
505
+ })
506
+
507
+ browserTest("browser: open without launching does not fetch or show requirement status", async () => {
508
+ const state = baseState()
509
+ await withBrowser(state, async ({ page, baseUrl }) => {
510
+ await page.goto(`${baseUrl}/open-without-launching`)
511
+ await page.waitForTimeout(250)
512
+
513
+ assert.equal(state.launchRequirementsGets, 0)
514
+ assert.equal(await page.locator("[data-launch-requirements-status]").isHidden(), true)
515
+ assert.equal(await page.locator("text=Preparing required apps").count(), 0)
516
+ })
517
+ })
518
+
519
+ browserTest("browser: home autolaunch status appears and transitions without refresh", async () => {
520
+ const state = baseState()
521
+ state.homeStatus = {
522
+ launch_complete: false,
523
+ running_apps: [],
524
+ running_scripts: [],
525
+ autolaunch: {
526
+ apps: {
527
+ target: {
528
+ id: "target",
529
+ title: "Target",
530
+ script: "start.js",
531
+ state: "waiting",
532
+ waiting_for: ["helper"],
533
+ dependencies: ["helper"]
534
+ },
535
+ helper: {
536
+ id: "helper",
537
+ title: "Helper",
538
+ state: "starting"
539
+ }
540
+ }
541
+ }
542
+ }
543
+ await withBrowser(state, async ({ page, baseUrl }) => {
544
+ await page.goto(`${baseUrl}/home-fixture`)
545
+ await page.waitForFunction(() => {
546
+ const chip = document.querySelector(".home-autolaunch-status")
547
+ return chip && chip.textContent.includes("Waiting for Helper")
548
+ })
549
+ assert.ok(state.homeStatusGets > 0)
550
+
551
+ state.homeStatus = {
552
+ launch_complete: true,
553
+ running_apps: ["target"],
554
+ running_scripts: [{
555
+ app: "target",
556
+ home_path: "api/target/start.js",
557
+ path: "target/start.js",
558
+ script_path: "start.js"
559
+ }],
560
+ autolaunch: {
561
+ apps: {
562
+ target: {
563
+ id: "target",
564
+ title: "Target",
565
+ script: "start.js",
566
+ state: "ready"
567
+ }
568
+ }
569
+ }
570
+ }
571
+ await page.waitForFunction(() => {
572
+ const button = document.querySelector(".home-app-line .shutdown")
573
+ return button && button.textContent.includes("Stop start.js")
574
+ })
575
+
576
+ assert.equal(await page.locator(".home-autolaunch-status").count(), 0)
577
+ assert.match(await textContent(page, ".home-app-line .shutdown"), /Stop start\.js/)
578
+ })
579
+ })