@fungi.computer/bonsai 0.0.1 → 0.2.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.
- package/CHANGELOG.md +92 -0
- package/README.md +66 -12
- package/backend/build-workflow.ts +137 -0
- package/backend/index.ts +13 -7
- package/backend/methods.ts +222 -19
- package/package.json +54 -18
- package/scripts/release.mjs +12 -1
- package/scripts/shed-terminal-proof.mjs +30 -28
- package/shared/contract.ts +41 -0
- package/test/apps-build-install.test.ts +399 -26
- package/test/browser/build-workflow.browser.mjs +104 -0
- package/test/browser/repository-browser.fixture.mjs +84 -0
- package/test/browser/repository-settings-fixture.tsx +198 -2
- package/test/browser/repository-settings.browser.mjs +188 -26
- package/test/checks-workflow-template.test.mjs +12 -10
- package/test/favicon.test.mjs +25 -0
- package/test/git-push.native.mjs +30 -0
- package/test/git-push.worker.ts +51 -3
- package/test/merge-template-build.test.mjs +44 -201
- package/test/present.test.ts +27 -0
- package/test/storyboard-save-https-git.fixture.mjs +35 -29
- package/test/storyboard-save-template.test.mjs +61 -12
- package/vite.config.ts +19 -1
- package/vitest.config.ts +43 -1
- package/web/app-build-panel.tsx +183 -95
- package/web/bonsai.css +1 -0
- package/web/change-page.tsx +10 -6
- package/web/index.html +0 -1
- package/web/repo-settings.tsx +1 -1
- package/web/public/favicon.svg +0 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
repositoryBrowserFixture,
|
|
3
|
+
repositoryBrowserPage,
|
|
4
|
+
startManualBuildWorkflow,
|
|
5
|
+
assertNoDirectInstall,
|
|
6
|
+
} from "./repository-browser.fixture.mjs";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import console from "node:console";
|
|
9
|
+
import { fileURLToPath, URL } from "node:url";
|
|
10
|
+
|
|
11
|
+
// Real Settings/button and Trellis cards, controlled App-method wire replies.
|
|
12
|
+
// This UI law does not claim a real host, Computer or installed Release.
|
|
13
|
+
const output = new URL(
|
|
14
|
+
"../../../../.botanical/honest-errors/manual-workflow-shots/",
|
|
15
|
+
import.meta.url,
|
|
16
|
+
);
|
|
17
|
+
const fixture = await repositoryBrowserFixture(output);
|
|
18
|
+
const { browser, origin } = fixture;
|
|
19
|
+
try {
|
|
20
|
+
for (const width of [1440, 390])
|
|
21
|
+
for (const theme of ["light", "dark"])
|
|
22
|
+
for (const refused of [false, true]) {
|
|
23
|
+
const { page, errors } = await repositoryBrowserPage(
|
|
24
|
+
browser,
|
|
25
|
+
width,
|
|
26
|
+
theme,
|
|
27
|
+
);
|
|
28
|
+
await page.goto(
|
|
29
|
+
origin +
|
|
30
|
+
"test/browser/repository-settings.html?scenario=" +
|
|
31
|
+
(refused ? "manual-workflow-refused" : "manual-workflow"),
|
|
32
|
+
);
|
|
33
|
+
await page.getByRole("link", { name: "Settings", exact: true }).click();
|
|
34
|
+
await page
|
|
35
|
+
.getByRole("button", { name: "Make this an App", exact: true })
|
|
36
|
+
.click();
|
|
37
|
+
await startManualBuildWorkflow(
|
|
38
|
+
page,
|
|
39
|
+
"git:" + "b".repeat(40),
|
|
40
|
+
"edited-installation",
|
|
41
|
+
);
|
|
42
|
+
await page
|
|
43
|
+
.getByText("Workflow run run_settings", { exact: true })
|
|
44
|
+
.waitFor();
|
|
45
|
+
if (refused) {
|
|
46
|
+
await page
|
|
47
|
+
.getByRole("alert")
|
|
48
|
+
.filter({
|
|
49
|
+
hasText: "No hosted build backend is attached to this Computer",
|
|
50
|
+
})
|
|
51
|
+
.waitFor();
|
|
52
|
+
assert.equal(
|
|
53
|
+
await page.getByText("Release installed", { exact: true }).count(),
|
|
54
|
+
0,
|
|
55
|
+
);
|
|
56
|
+
assert.equal(
|
|
57
|
+
await page.getByText("Release verified", { exact: true }).count(),
|
|
58
|
+
0,
|
|
59
|
+
);
|
|
60
|
+
} else {
|
|
61
|
+
await page.getByText("Build running", { exact: true }).waitFor();
|
|
62
|
+
await page.getByText("Release verified", { exact: true }).waitFor();
|
|
63
|
+
await page.getByText("Release installed", { exact: true }).waitFor();
|
|
64
|
+
assert.equal(
|
|
65
|
+
await page.locator(".wf-step").count(),
|
|
66
|
+
3,
|
|
67
|
+
"the retained Trellis step cards render the actual step rows",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const calls = JSON.parse(
|
|
71
|
+
await page.locator("#app-calls").textContent(),
|
|
72
|
+
);
|
|
73
|
+
assert.equal(
|
|
74
|
+
calls.filter((name) => name === "apps.startBuildWorkflow").length,
|
|
75
|
+
1,
|
|
76
|
+
);
|
|
77
|
+
assert.ok(calls.includes("apps.buildWorkflowRun"));
|
|
78
|
+
assert.ok(
|
|
79
|
+
!calls.includes("apps.build"),
|
|
80
|
+
"the button must start the Workflow, not a separate native build",
|
|
81
|
+
);
|
|
82
|
+
assert.ok(
|
|
83
|
+
!calls.includes("apps.install"),
|
|
84
|
+
"the panel must never select a Release as an App",
|
|
85
|
+
);
|
|
86
|
+
await assertNoDirectInstall(page);
|
|
87
|
+
assert.deepEqual(errors, []);
|
|
88
|
+
await page.screenshot({
|
|
89
|
+
path: fileURLToPath(
|
|
90
|
+
new URL(
|
|
91
|
+
width + "-" + theme + (refused ? "-refused" : "") + ".png",
|
|
92
|
+
output,
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
fullPage: true,
|
|
96
|
+
});
|
|
97
|
+
await page.close();
|
|
98
|
+
}
|
|
99
|
+
console.log(
|
|
100
|
+
"PASS manual Build & install starts one Workflow, renders Trellis cards and preserves refusal reasons in four views",
|
|
101
|
+
);
|
|
102
|
+
} finally {
|
|
103
|
+
await fixture.close();
|
|
104
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { createServer } from "vite";
|
|
4
|
+
import { chromium } from "playwright";
|
|
5
|
+
import viteConfig from "./vite.config.mjs";
|
|
6
|
+
|
|
7
|
+
/** Serve the real Settings view and its controlled App-method transport. */
|
|
8
|
+
export async function repositoryBrowserFixture(output) {
|
|
9
|
+
await mkdir(output, { recursive: true });
|
|
10
|
+
const server = await createServer({
|
|
11
|
+
...viteConfig,
|
|
12
|
+
configFile: false,
|
|
13
|
+
server: { ...viteConfig.server, port: 0 },
|
|
14
|
+
});
|
|
15
|
+
await server.listen();
|
|
16
|
+
try {
|
|
17
|
+
const browser = await chromium.launch({ headless: true });
|
|
18
|
+
return {
|
|
19
|
+
browser,
|
|
20
|
+
origin: server.resolvedUrls.local[0],
|
|
21
|
+
async close() {
|
|
22
|
+
try {
|
|
23
|
+
await browser.close();
|
|
24
|
+
} finally {
|
|
25
|
+
await server.close();
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
} catch (cause) {
|
|
30
|
+
await server.close();
|
|
31
|
+
throw cause;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function repositoryBrowserPage(browser, width, theme) {
|
|
36
|
+
const page = await browser.newPage({
|
|
37
|
+
viewport: { width, height: 1000 },
|
|
38
|
+
colorScheme: theme,
|
|
39
|
+
});
|
|
40
|
+
page.setDefaultTimeout(10000);
|
|
41
|
+
const errors = [];
|
|
42
|
+
page.on("pageerror", (cause) => errors.push(cause.message));
|
|
43
|
+
return { page, errors };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Expected identity stays with each scenario; inspect the same visible fields. */
|
|
47
|
+
export async function assertAuthoringFields(page, expected) {
|
|
48
|
+
for (const [name, value] of [
|
|
49
|
+
["App id", expected.appId],
|
|
50
|
+
["Source revision", expected.sourceRevision],
|
|
51
|
+
["Install at", expected.installationId],
|
|
52
|
+
]) {
|
|
53
|
+
assert.equal(
|
|
54
|
+
await page.getByRole("textbox", { name, exact: true }).inputValue(),
|
|
55
|
+
value,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function startManualBuildWorkflow(
|
|
61
|
+
page,
|
|
62
|
+
sourceRevision,
|
|
63
|
+
installationId,
|
|
64
|
+
) {
|
|
65
|
+
await page
|
|
66
|
+
.getByRole("textbox", { name: "Source revision", exact: true })
|
|
67
|
+
.fill(sourceRevision);
|
|
68
|
+
await page
|
|
69
|
+
.getByRole("textbox", { name: "Install at", exact: true })
|
|
70
|
+
.fill(installationId);
|
|
71
|
+
await page
|
|
72
|
+
.getByRole("button", { name: "Build & install", exact: true })
|
|
73
|
+
.click();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function assertNoDirectInstall(page) {
|
|
77
|
+
assert.equal(
|
|
78
|
+
await page
|
|
79
|
+
.getByRole("button", { name: "Install this Release", exact: true })
|
|
80
|
+
.count(),
|
|
81
|
+
0,
|
|
82
|
+
"Only the Workflow may install; no direct install action remains",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
import { createRoot } from "react-dom/client";
|
|
4
4
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
5
|
-
import { createCapabilityRegistry } from "@fungi.computer/agar";
|
|
5
|
+
import { createCapabilityRegistry, pending } from "@fungi.computer/agar";
|
|
6
6
|
import { appMethods } from "@fungi.computer/agar/backend";
|
|
7
7
|
import * as z from "zod";
|
|
8
|
-
import { repoDetail } from "../../shared/contract.js";
|
|
8
|
+
import { appBuildStatusInput, repoDetail } from "../../shared/contract.js";
|
|
9
9
|
import { connect } from "../../web/transport.js";
|
|
10
10
|
import { RouterProvider, useRouter } from "../../web/router.js";
|
|
11
11
|
import { RepoPage } from "../../web/pages.js";
|
|
@@ -39,10 +39,203 @@ client.setQueryData(["events", name], { events: [] });
|
|
|
39
39
|
client.setQueryData(["changes", name], { changes: [] });
|
|
40
40
|
client.setQueryData(["agents"], { agents: [] });
|
|
41
41
|
let attempts = 0;
|
|
42
|
+
let registrations = 0;
|
|
42
43
|
let withdrawn = false;
|
|
44
|
+
const appCalls: string[] = [];
|
|
45
|
+
const build = {
|
|
46
|
+
jobId: "build_settings",
|
|
47
|
+
appId: "app_settingstest",
|
|
48
|
+
releaseId: "rel_settings",
|
|
49
|
+
sourceRevision: "git:" + "a".repeat(40),
|
|
50
|
+
status: "running",
|
|
51
|
+
detail: null,
|
|
52
|
+
release: null,
|
|
53
|
+
installation: { kind: "pending", installationId: name },
|
|
54
|
+
};
|
|
43
55
|
const capabilities = createCapabilityRegistry();
|
|
44
56
|
capabilities.provide(appMethods, {
|
|
45
57
|
call: (method, inputJson) => {
|
|
58
|
+
if (method.startsWith("apps.")) {
|
|
59
|
+
appCalls.push(method);
|
|
60
|
+
const counter = document.getElementById("app-calls");
|
|
61
|
+
if (counter) counter.textContent = JSON.stringify(appCalls);
|
|
62
|
+
}
|
|
63
|
+
if (method === "apps.register") {
|
|
64
|
+
z.strictObject({ repo: z.literal(name) }).parse(JSON.parse(inputJson));
|
|
65
|
+
registrations += 1;
|
|
66
|
+
const scenario = new URLSearchParams(location.search).get("scenario");
|
|
67
|
+
if (scenario === "registration-refused")
|
|
68
|
+
return Promise.resolve({ error: "Owner only" });
|
|
69
|
+
if (scenario === "registration-invalid")
|
|
70
|
+
return Promise.resolve({
|
|
71
|
+
error: "Repository main must declare a valid fungi.build recipe",
|
|
72
|
+
});
|
|
73
|
+
if (scenario === "registration-unavailable" && registrations === 1)
|
|
74
|
+
return Promise.resolve({ error: "App registration is unavailable" });
|
|
75
|
+
return Promise.resolve({
|
|
76
|
+
appId: "app_settingstest",
|
|
77
|
+
sourceRevision: "git:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (method === "apps.startBuildWorkflow") {
|
|
81
|
+
z.strictObject({
|
|
82
|
+
repo: z.literal(name),
|
|
83
|
+
sourceRevision: z.literal("git:" + "b".repeat(40)),
|
|
84
|
+
installationId: z.literal("edited-installation"),
|
|
85
|
+
requestId: z.string().min(1).max(256),
|
|
86
|
+
}).parse(JSON.parse(inputJson));
|
|
87
|
+
return Promise.resolve({
|
|
88
|
+
kind: "started",
|
|
89
|
+
runId: "run_settings",
|
|
90
|
+
planDigest: "sha256:" + "d".repeat(64),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (method === "apps.buildWorkflowRun") {
|
|
94
|
+
z.strictObject({ runId: z.literal("run_settings") }).parse(
|
|
95
|
+
JSON.parse(inputJson),
|
|
96
|
+
);
|
|
97
|
+
if (
|
|
98
|
+
new URLSearchParams(location.search).get("scenario") ===
|
|
99
|
+
"manual-workflow-refused"
|
|
100
|
+
)
|
|
101
|
+
return Promise.resolve({
|
|
102
|
+
runId: "run_settings",
|
|
103
|
+
planDigest: "sha256:" + "d".repeat(64),
|
|
104
|
+
outcome: "terminal",
|
|
105
|
+
title: "Build on merge",
|
|
106
|
+
status: "errored",
|
|
107
|
+
output: null,
|
|
108
|
+
error: {
|
|
109
|
+
name: "Error",
|
|
110
|
+
message:
|
|
111
|
+
"App method refused (unavailable): No hosted build backend is attached to this Computer",
|
|
112
|
+
},
|
|
113
|
+
reviews: [],
|
|
114
|
+
events: [
|
|
115
|
+
{
|
|
116
|
+
nodeId: "build",
|
|
117
|
+
iteration: "0",
|
|
118
|
+
poll: 0,
|
|
119
|
+
state: "failed",
|
|
120
|
+
attempt: 1,
|
|
121
|
+
refusal: "dispatch_failed",
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
const release = {
|
|
126
|
+
appId: "app_settingstest",
|
|
127
|
+
releaseId: "rel_settings",
|
|
128
|
+
artifact: {
|
|
129
|
+
artifactRef: "artifact:sha256-" + "b".repeat(64),
|
|
130
|
+
artifactDigest: "sha256:" + "b".repeat(64),
|
|
131
|
+
provenance: {
|
|
132
|
+
sourceRevision: "git:" + "b".repeat(40),
|
|
133
|
+
sourceTreeDigest: "sha256:" + "c".repeat(64),
|
|
134
|
+
buildRecipeDigest: "sha256:" + "d".repeat(64),
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
return Promise.resolve({
|
|
139
|
+
runId: "run_settings",
|
|
140
|
+
planDigest: "sha256:" + "d".repeat(64),
|
|
141
|
+
outcome: "terminal",
|
|
142
|
+
title: "Build on merge",
|
|
143
|
+
status: "complete",
|
|
144
|
+
output: {
|
|
145
|
+
installation: {
|
|
146
|
+
installationId: "edited-installation",
|
|
147
|
+
status: "enabled",
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
error: null,
|
|
151
|
+
reviews: [],
|
|
152
|
+
events: [
|
|
153
|
+
{
|
|
154
|
+
nodeId: "build",
|
|
155
|
+
iteration: "0",
|
|
156
|
+
state: "complete",
|
|
157
|
+
attempt: 0,
|
|
158
|
+
output: {
|
|
159
|
+
...build,
|
|
160
|
+
sourceRevision: "git:" + "b".repeat(40),
|
|
161
|
+
installation: {
|
|
162
|
+
kind: "pending",
|
|
163
|
+
installationId: "edited-installation",
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
row: {
|
|
167
|
+
id: "build_settings",
|
|
168
|
+
title: "Build running",
|
|
169
|
+
detail: "git:" + "b".repeat(40),
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
nodeId: "verify",
|
|
174
|
+
iteration: "0",
|
|
175
|
+
state: "complete",
|
|
176
|
+
attempt: 0,
|
|
177
|
+
output: {
|
|
178
|
+
release,
|
|
179
|
+
summary:
|
|
180
|
+
"Built rel_settings of app_settingstest from git:" +
|
|
181
|
+
"b".repeat(40),
|
|
182
|
+
},
|
|
183
|
+
row: {
|
|
184
|
+
id: "rel_settings",
|
|
185
|
+
title: "Release verified",
|
|
186
|
+
detail: "Exact source verified",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
nodeId: "install",
|
|
191
|
+
iteration: "0",
|
|
192
|
+
state: "complete",
|
|
193
|
+
attempt: 0,
|
|
194
|
+
output: {
|
|
195
|
+
installationId: "edited-installation",
|
|
196
|
+
status: "enabled",
|
|
197
|
+
},
|
|
198
|
+
row: {
|
|
199
|
+
id: "apps.install",
|
|
200
|
+
title: "Release installed",
|
|
201
|
+
detail: "edited-installation",
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (method === "apps.buildStatus") {
|
|
208
|
+
const input = appBuildStatusInput.parse(JSON.parse(inputJson));
|
|
209
|
+
if (
|
|
210
|
+
new URLSearchParams(location.search).get("scenario") ===
|
|
211
|
+
"automatic-pending"
|
|
212
|
+
)
|
|
213
|
+
return Promise.resolve(pending(5000));
|
|
214
|
+
if (
|
|
215
|
+
"repo" in input &&
|
|
216
|
+
new URLSearchParams(location.search).get("scenario") !==
|
|
217
|
+
"automatic-needs-consent"
|
|
218
|
+
)
|
|
219
|
+
return Promise.resolve(null);
|
|
220
|
+
return Promise.resolve({
|
|
221
|
+
...build,
|
|
222
|
+
status: "succeeded",
|
|
223
|
+
installation: { kind: "needs_consent", installationId: name },
|
|
224
|
+
release: {
|
|
225
|
+
appId: build.appId,
|
|
226
|
+
releaseId: build.releaseId,
|
|
227
|
+
artifact: {
|
|
228
|
+
artifactRef: "artifact:sha256-" + "b".repeat(64),
|
|
229
|
+
artifactDigest: "sha256:" + "b".repeat(64),
|
|
230
|
+
provenance: {
|
|
231
|
+
sourceRevision: build.sourceRevision,
|
|
232
|
+
sourceTreeDigest: "sha256:" + "c".repeat(64),
|
|
233
|
+
buildRecipeDigest: "sha256:" + "d".repeat(64),
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
}
|
|
46
239
|
if (method === "repos.delete") {
|
|
47
240
|
z.strictObject({ repo: z.literal(name) }).parse(JSON.parse(inputJson));
|
|
48
241
|
attempts += 1;
|
|
@@ -91,6 +284,9 @@ function Shell() {
|
|
|
91
284
|
<h1>Repositories</h1>
|
|
92
285
|
)}
|
|
93
286
|
</main>
|
|
287
|
+
<output id="app-calls" hidden>
|
|
288
|
+
{JSON.stringify([])}
|
|
289
|
+
</output>
|
|
94
290
|
<output id="attempts" hidden>
|
|
95
291
|
0
|
|
96
292
|
</output>
|
|
@@ -1,39 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
repositoryBrowserFixture,
|
|
3
|
+
repositoryBrowserPage,
|
|
4
|
+
startManualBuildWorkflow,
|
|
5
|
+
assertNoDirectInstall,
|
|
6
|
+
assertAuthoringFields,
|
|
7
|
+
} from "./repository-browser.fixture.mjs";
|
|
1
8
|
import assert from "node:assert/strict";
|
|
2
9
|
import console from "node:console";
|
|
3
10
|
import process from "node:process";
|
|
4
|
-
import { mkdir } from "node:fs/promises";
|
|
5
11
|
import { fileURLToPath, URL } from "node:url";
|
|
6
|
-
import { createServer } from "vite";
|
|
7
|
-
import { chromium } from "playwright";
|
|
8
|
-
import viteConfig from "./vite.config.mjs";
|
|
9
12
|
|
|
10
13
|
const output = new URL(
|
|
11
14
|
"../../../../.botanical/repos-delete/shots/",
|
|
12
15
|
import.meta.url,
|
|
13
16
|
);
|
|
14
|
-
await
|
|
15
|
-
const
|
|
16
|
-
...viteConfig,
|
|
17
|
-
configFile: false,
|
|
18
|
-
server: { ...viteConfig.server, port: 0 },
|
|
19
|
-
});
|
|
20
|
-
await server.listen();
|
|
21
|
-
const origin = server.resolvedUrls.local[0];
|
|
22
|
-
let browser;
|
|
17
|
+
const fixture = await repositoryBrowserFixture(output);
|
|
18
|
+
const { browser, origin } = fixture;
|
|
23
19
|
const name = "disposable-merge-1791840651624";
|
|
24
20
|
const before = process.argv.includes("--before");
|
|
21
|
+
const appWrites = async (page) =>
|
|
22
|
+
JSON.parse(await page.locator("#app-calls").textContent()).filter(
|
|
23
|
+
(method) =>
|
|
24
|
+
method !== "apps.buildStatus" && method !== "apps.buildWorkflowRun",
|
|
25
|
+
);
|
|
25
26
|
try {
|
|
26
|
-
browser = await chromium.launch({ headless: true });
|
|
27
27
|
for (const width of [1440, 390])
|
|
28
28
|
for (const theme of ["light", "dark"]) {
|
|
29
|
-
const page = await
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
const { page, errors } = await repositoryBrowserPage(
|
|
30
|
+
browser,
|
|
31
|
+
width,
|
|
32
|
+
theme,
|
|
33
|
+
);
|
|
34
|
+
if (!before) {
|
|
35
|
+
await page.goto(
|
|
36
|
+
origin +
|
|
37
|
+
"test/browser/repository-settings.html?scenario=automatic-needs-consent",
|
|
38
|
+
);
|
|
39
|
+
for (let visit = 0; visit < 2; visit++) {
|
|
40
|
+
if (visit > 0) await page.reload();
|
|
41
|
+
await page
|
|
42
|
+
.getByRole("link", { name: "Settings", exact: true })
|
|
43
|
+
.click();
|
|
44
|
+
await page
|
|
45
|
+
.getByRole("status")
|
|
46
|
+
.filter({ hasText: "Needs owner consent" })
|
|
47
|
+
.waitFor();
|
|
48
|
+
await assertAuthoringFields(page, {
|
|
49
|
+
appId: "app_settingstest",
|
|
50
|
+
sourceRevision: "git:" + "a".repeat(40),
|
|
51
|
+
installationId: name,
|
|
52
|
+
});
|
|
53
|
+
await assertNoDirectInstall(page);
|
|
54
|
+
assert.equal(
|
|
55
|
+
await page
|
|
56
|
+
.getByRole("button", { name: "Build & install", exact: true })
|
|
57
|
+
.isEnabled(),
|
|
58
|
+
true,
|
|
59
|
+
);
|
|
60
|
+
assert.deepEqual(
|
|
61
|
+
await appWrites(page),
|
|
62
|
+
[],
|
|
63
|
+
"Status discovery and reload must not register, build or install",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
await page.screenshot({
|
|
67
|
+
path: fileURLToPath(
|
|
68
|
+
new URL(width + "-" + theme + "-automatic-reload.png", output),
|
|
69
|
+
),
|
|
70
|
+
fullPage: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (!before) {
|
|
74
|
+
await page.goto(
|
|
75
|
+
origin +
|
|
76
|
+
"test/browser/repository-settings.html?scenario=automatic-pending",
|
|
77
|
+
);
|
|
78
|
+
await page.getByRole("link", { name: "Settings", exact: true }).click();
|
|
79
|
+
await page
|
|
80
|
+
.getByRole("status")
|
|
81
|
+
.filter({
|
|
82
|
+
hasText: "Build or automatic installation is still running",
|
|
83
|
+
})
|
|
84
|
+
.waitFor();
|
|
85
|
+
assert.deepEqual(await appWrites(page), []);
|
|
86
|
+
}
|
|
87
|
+
await page.goto(
|
|
88
|
+
origin +
|
|
89
|
+
"test/browser/repository-settings.html?scenario=manual-workflow-refused",
|
|
90
|
+
);
|
|
37
91
|
await page.evaluate(
|
|
38
92
|
(value) =>
|
|
39
93
|
globalThis.document.documentElement.setAttribute("data-theme", value),
|
|
@@ -56,10 +110,65 @@ try {
|
|
|
56
110
|
await page
|
|
57
111
|
.getByRole("heading", { name: "Repository settings", exact: true })
|
|
58
112
|
.waitFor();
|
|
113
|
+
await page
|
|
114
|
+
.getByRole("button", { name: "Make this an App", exact: true })
|
|
115
|
+
.click();
|
|
116
|
+
await page
|
|
117
|
+
.locator('#app-build-app-id[value="app_settingstest"]')
|
|
118
|
+
.waitFor();
|
|
119
|
+
await assertAuthoringFields(page, {
|
|
120
|
+
appId: "app_settingstest",
|
|
121
|
+
sourceRevision: "git:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
122
|
+
installationId: name,
|
|
123
|
+
});
|
|
124
|
+
await page
|
|
125
|
+
.getByRole("button", { name: "Build & install", exact: true })
|
|
126
|
+
.and(page.locator(":enabled"))
|
|
127
|
+
.waitFor();
|
|
128
|
+
assert.equal(
|
|
129
|
+
await page
|
|
130
|
+
.getByRole("button", { name: "Build & install", exact: true })
|
|
131
|
+
.isEnabled(),
|
|
132
|
+
true,
|
|
133
|
+
);
|
|
134
|
+
await assertNoDirectInstall(page);
|
|
135
|
+
assert.deepEqual(await appWrites(page), ["apps.register"]);
|
|
59
136
|
assert.equal(await page.locator("#attempts").textContent(), "0");
|
|
60
137
|
await page.screenshot({
|
|
61
138
|
path: fileURLToPath(new URL(`${width}-${theme}-settings.png`, output)),
|
|
62
139
|
});
|
|
140
|
+
assert.equal(
|
|
141
|
+
await page
|
|
142
|
+
.getByRole("textbox", { name: "App id", exact: true })
|
|
143
|
+
.isEditable(),
|
|
144
|
+
false,
|
|
145
|
+
);
|
|
146
|
+
await startManualBuildWorkflow(
|
|
147
|
+
page,
|
|
148
|
+
"git:" + "b".repeat(40),
|
|
149
|
+
"edited-installation",
|
|
150
|
+
);
|
|
151
|
+
await page
|
|
152
|
+
.getByRole("alert")
|
|
153
|
+
.filter({
|
|
154
|
+
hasText: "No hosted build backend is attached to this Computer",
|
|
155
|
+
})
|
|
156
|
+
.waitFor();
|
|
157
|
+
await assertAuthoringFields(page, {
|
|
158
|
+
appId: "app_settingstest",
|
|
159
|
+
sourceRevision: "git:" + "b".repeat(40),
|
|
160
|
+
installationId: "edited-installation",
|
|
161
|
+
});
|
|
162
|
+
assert.deepEqual(await appWrites(page), [
|
|
163
|
+
"apps.register",
|
|
164
|
+
"apps.startBuildWorkflow",
|
|
165
|
+
]);
|
|
166
|
+
await assertNoDirectInstall(page);
|
|
167
|
+
await page.screenshot({
|
|
168
|
+
path: fileURLToPath(
|
|
169
|
+
new URL(`${width}-${theme}-build-refusal.png`, output),
|
|
170
|
+
),
|
|
171
|
+
});
|
|
63
172
|
await page
|
|
64
173
|
.getByRole("button", { name: "Delete repository", exact: true })
|
|
65
174
|
.click();
|
|
@@ -88,7 +197,16 @@ try {
|
|
|
88
197
|
hasText: "Deletion didn't finish because storage was unavailable.",
|
|
89
198
|
})
|
|
90
199
|
.waitFor();
|
|
91
|
-
assert.equal(
|
|
200
|
+
assert.equal(
|
|
201
|
+
await page
|
|
202
|
+
.getByRole("alert")
|
|
203
|
+
.filter({
|
|
204
|
+
hasText: "Deletion didn't finish because storage was unavailable.",
|
|
205
|
+
})
|
|
206
|
+
.locator("p")
|
|
207
|
+
.count(),
|
|
208
|
+
1,
|
|
209
|
+
);
|
|
92
210
|
assert.equal(await page.locator("#attempts").textContent(), "1");
|
|
93
211
|
await page.screenshot({
|
|
94
212
|
path: fileURLToPath(new URL(`${width}-${theme}-retry.png`, output)),
|
|
@@ -111,6 +229,51 @@ try {
|
|
|
111
229
|
.getByRole("alert")
|
|
112
230
|
.filter({ hasText: "Only the owner" })
|
|
113
231
|
.waitFor();
|
|
232
|
+
for (const [scenario, message] of [
|
|
233
|
+
["registration-refused", "Owner only"],
|
|
234
|
+
[
|
|
235
|
+
"registration-invalid",
|
|
236
|
+
"Repository main must declare a valid fungi.build recipe",
|
|
237
|
+
],
|
|
238
|
+
["registration-unavailable", "App registration is unavailable"],
|
|
239
|
+
]) {
|
|
240
|
+
await page.goto(
|
|
241
|
+
origin + "test/browser/repository-settings.html?scenario=" + scenario,
|
|
242
|
+
);
|
|
243
|
+
await page.getByRole("link", { name: "Settings", exact: true }).click();
|
|
244
|
+
await page
|
|
245
|
+
.getByRole("button", { name: "Make this an App", exact: true })
|
|
246
|
+
.click();
|
|
247
|
+
await page.getByRole("alert").filter({ hasText: message }).waitFor();
|
|
248
|
+
|
|
249
|
+
await assertAuthoringFields(page, {
|
|
250
|
+
appId: "",
|
|
251
|
+
sourceRevision: "",
|
|
252
|
+
installationId: name,
|
|
253
|
+
});
|
|
254
|
+
assert.equal(
|
|
255
|
+
await page
|
|
256
|
+
.getByRole("button", { name: "Build & install", exact: true })
|
|
257
|
+
.isDisabled(),
|
|
258
|
+
true,
|
|
259
|
+
);
|
|
260
|
+
await assertNoDirectInstall(page);
|
|
261
|
+
assert.deepEqual(await appWrites(page), ["apps.register"]);
|
|
262
|
+
await page.screenshot({
|
|
263
|
+
path: fileURLToPath(
|
|
264
|
+
new URL([width, theme, scenario].join("-") + ".png", output),
|
|
265
|
+
),
|
|
266
|
+
});
|
|
267
|
+
if (scenario === "registration-unavailable") {
|
|
268
|
+
await page
|
|
269
|
+
.getByRole("button", { name: "Make this an App", exact: true })
|
|
270
|
+
.click();
|
|
271
|
+
await page
|
|
272
|
+
.locator('#app-build-app-id[value="app_settingstest"]')
|
|
273
|
+
.waitFor();
|
|
274
|
+
assert.equal(await page.getByRole("alert").count(), 0);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
114
277
|
assert.deepEqual(errors, []);
|
|
115
278
|
assert.equal(
|
|
116
279
|
await page.evaluate(
|
|
@@ -126,11 +289,10 @@ try {
|
|
|
126
289
|
console.log(
|
|
127
290
|
before
|
|
128
291
|
? "PASS baseline 1440/390 light/dark: repository view before Settings"
|
|
129
|
-
: "PASS Settings 1440/390 light/dark:
|
|
292
|
+
: "PASS Settings 1440/390 light/dark: App registration/prefill, readonly identity, editable head/name and honest build refusal; typed needs-consent status without installing; denied/invalid/unavailable registration and retry; deletion confirmation/cancel/refusal/provider retry/navigation",
|
|
130
293
|
);
|
|
131
294
|
} finally {
|
|
132
|
-
await
|
|
133
|
-
await server.close();
|
|
295
|
+
await fixture.close();
|
|
134
296
|
console.log("CLOSED browser and Vite server");
|
|
135
297
|
}
|
|
136
298
|
process.exitCode = 0;
|