Skip to content

Commit ef798b0

Browse files
yyodsclaude
andcommitted
[red] pin five-profile flash page copy to its frozen spec
For a selected five-profile v0.6.0 release, /flash must not render the stale 'Planned profiles: Unavailable' cards beside an installer offering those same targets. Pin instead: the five-target provisioning summary (candidate mode framed as a visibly pending candidate), all five profile requirements, the split Web Serial vs Pico Web Crypto browser requirements, all four ESP merged-image esptool commands with the C3 0x0 component offset, and the separate Pico BOOTSEL re-copy recovery paragraph with no esptool/offset/Web Serial fallback (browser-flashing §7/§8). Also pin the exact-board Waveshare warning to profile presence in the selected release, Pico-appropriate consent wording, a visible consent legend, no aria-disabled on informational cards, no pre-GP2 jargon, and Blockly/Classic ESP32/ESP Web Tools naming. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016KVSKkd7KUMHqKXDhPXTj5 Signed-off-by: Viwat Vchirawongkwin <viwat.v@chula.ac.th>
1 parent 0c7230d commit ef798b0

4 files changed

Lines changed: 289 additions & 8 deletions

File tree

‎tools/web/src/test/firmware-v060-qualified-release.test.tsx‎

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ interface QualifiedProfileDescriptor {
5959
}
6060

6161
interface QualifiedReleaseDescriptor {
62-
deployment: "public";
63-
accessControlled: false;
62+
deployment: "public" | "candidate";
63+
accessControlled: boolean;
6464
version: string;
6565
builtAt: string;
6666
hilStatus: HilStatus;
@@ -191,6 +191,14 @@ function qualifiedRelease(
191191
};
192192
}
193193

194+
function candidateRelease(): QualifiedReleaseDescriptor {
195+
return {
196+
...qualifiedRelease("pending"),
197+
deployment: "candidate",
198+
accessControlled: true,
199+
};
200+
}
201+
194202
function textBytes(value: string) {
195203
return encoder.encode(value);
196204
}
@@ -684,6 +692,75 @@ describe("qualified v0.6.0 production installer", () => {
684692
expect(document.querySelector("esp-web-install-button")).toBeNull();
685693
});
686694

695+
it("keeps the exact-board Waveshare warning visible whenever its profile is selectable", () => {
696+
const candidate = render(
697+
<QualifiedFlashStatus
698+
capabilities={{ secureContext: true, webSerial: true, webCrypto: true }}
699+
release={candidateRelease()}
700+
/>,
701+
);
702+
703+
expect(
704+
screen.getByRole("heading", { name: /protected release candidate/i }),
705+
).toBeVisible();
706+
expect(
707+
screen.getByText(/image is only for the exact ESP32-S3-LCD-1\.47B/i),
708+
).toBeVisible();
709+
candidate.unmount();
710+
711+
render(
712+
<QualifiedFlashStatus
713+
capabilities={{ secureContext: true, webSerial: true, webCrypto: true }}
714+
release={qualifiedRelease()}
715+
/>,
716+
);
717+
expect(
718+
screen.getByText(/image is only for the exact ESP32-S3-LCD-1\.47B/i),
719+
).toBeVisible();
720+
});
721+
722+
it("asks for Pico-appropriate acknowledgements in the verified-UF2 flow", () => {
723+
render(
724+
<QualifiedFlashStatus
725+
capabilities={{
726+
secureContext: true,
727+
webSerial: false,
728+
webCrypto: true,
729+
}}
730+
release={qualifiedRelease()}
731+
/>,
732+
);
733+
734+
selectProfile("rpi-pico2-w");
735+
736+
const group = screen.getByRole("group", {
737+
name: /installation acknowledgements/i,
738+
});
739+
expect(
740+
within(group).getByText("Installation acknowledgements", {
741+
selector: "legend",
742+
}),
743+
).toBeInTheDocument();
744+
// The board affirmation names the actual Pico 2 W hardware contract, not
745+
// the ESP chip/silicon-revision/flash/PSRAM fields the schema-4 Pico
746+
// profile deliberately omits.
747+
expect(
748+
within(group).getByRole("checkbox", {
749+
name: /raspberry pi pico 2 w \(rp2350 \+ cyw43439\)/i,
750+
}),
751+
).toBeInTheDocument();
752+
expect(
753+
within(group).queryByRole("checkbox", {
754+
name: /chip, silicon revision, flash, and psram/i,
755+
}),
756+
).toBeNull();
757+
expect(
758+
within(group).queryByRole("checkbox", {
759+
name: /closed serial monitors/i,
760+
}),
761+
).toBeNull();
762+
});
763+
687764
it("keeps a pending public five-profile release fail-closed", () => {
688765
render(
689766
<QualifiedFlashStatus

‎tools/web/src/test/flash-installer.test.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,7 +1021,7 @@ describe("local five-board engineering preview", () => {
10211021
"rpi-pico2-w",
10221022
]);
10231023
expect(targetOptions.map((option) => option.textContent)).toEqual([
1024-
expect.stringMatching(/classical esp32/i),
1024+
expect.stringMatching(/classic esp32/i),
10251025
expect.stringMatching(/esp32-s3.*lean/i),
10261026
expect.stringMatching(/waveshare.*exact/i),
10271027
expect.stringMatching(/esp32-c3/i),
@@ -1055,7 +1055,7 @@ describe("local five-board engineering preview", () => {
10551055
name: /selected firmware target details/i,
10561056
});
10571057
expect(details).toHaveTextContent(/esp32-c3-4mb/i);
1058-
expect(details).toHaveTextContent(/esp32 web tools.*web serial/i);
1058+
expect(details).toHaveTextContent(/\besp web tools\b.*web serial/i);
10591059
expect(details).toHaveTextContent(
10601060
/local engineering preview.*unqualified/i,
10611061
);
@@ -1119,7 +1119,7 @@ describe("local five-board engineering preview", () => {
11191119
acceptPreviewConsents();
11201120
fireEvent.click(
11211121
screen.getByRole("button", {
1122-
name: /verify firmware.*classical esp32/i,
1122+
name: /verify firmware.*classic esp32/i,
11231123
}),
11241124
);
11251125

@@ -1197,7 +1197,7 @@ describe("local five-board engineering preview", () => {
11971197
acceptPreviewConsents();
11981198
fireEvent.click(
11991199
screen.getByRole("button", {
1200-
name: /verify firmware.*classical esp32/i,
1200+
name: /verify firmware.*classic esp32/i,
12011201
}),
12021202
);
12031203
await waitFor(() => {

‎tools/web/src/test/flash-page-release-copy.test.tsx‎

Lines changed: 202 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import { render, screen, within } from "@testing-library/react";
99
import { describe, expect, it, vi } from "vitest";
1010

1111
import FlashPage from "@/app/flash/page";
12-
import type { FirmwareReleaseDescriptor } from "@/lib/firmware-release";
12+
import {
13+
firmwareProfileDescriptors,
14+
type FirmwareReleaseDescriptor,
15+
} from "@/lib/firmware-release";
1316
import * as firmwareSelection from "@/lib/firmware-release-selection";
1417
import type { LocalFirmwarePreviewDescriptor } from "@/lib/local-firmware-preview";
1518
import {
@@ -48,6 +51,164 @@ function hypotheticalQualifiedReleaseAtVersion(
4851
return release as unknown as FirmwareReleaseDescriptor;
4952
}
5053

54+
function fiveProfileV060Release(
55+
deployment: "candidate" | "public",
56+
): FirmwareReleaseDescriptor {
57+
const version = "0.6.0";
58+
return {
59+
deployment,
60+
accessControlled: deployment === "candidate",
61+
version,
62+
builtAt: "2026-08-21T00:00:00Z",
63+
hilStatus: deployment === "public" ? "passed" : "pending",
64+
releaseJson: {
65+
path: `/firmware/v${version}/release.json`,
66+
sha256: "a".repeat(64),
67+
},
68+
schemaPath: `/firmware/v${version}/release.schema.json`,
69+
recoveryPath: `/firmware/v${version}/RECOVERY.md`,
70+
profiles: firmwareProfileDescriptors(version),
71+
} as FirmwareReleaseDescriptor;
72+
}
73+
74+
async function withSelectedRelease(
75+
release: FirmwareReleaseDescriptor,
76+
run: () => Promise<void> | void,
77+
) {
78+
const selectionRoot = await mkdtemp(
79+
join(tmpdir(), "pyble-flash-five-profile-selection-"),
80+
);
81+
const selectionFile = join(selectionRoot, "selection.json");
82+
const previousSelection = process.env.PYBLE_FLASH_SELECTION_FILE;
83+
process.env.PYBLE_FLASH_SELECTION_FILE = selectionFile;
84+
try {
85+
await writeFile(selectionFile, JSON.stringify(release), "utf8");
86+
await run();
87+
} finally {
88+
if (previousSelection === undefined) {
89+
delete process.env.PYBLE_FLASH_SELECTION_FILE;
90+
} else {
91+
process.env.PYBLE_FLASH_SELECTION_FILE = previousSelection;
92+
}
93+
await rm(selectionRoot, { recursive: true, force: true });
94+
}
95+
}
96+
97+
function expectFiveProfileFlashPageCopy() {
98+
// The stale planned/unavailable cards must never render beside a
99+
// five-profile installer that offers those same targets.
100+
expect(
101+
screen.queryByRole("heading", { name: /planned profiles/i }),
102+
).toBeNull();
103+
expect(
104+
screen.queryByText(
105+
/no installer selection, firmware image, or recovery command is offered yet/i,
106+
),
107+
).toBeNull();
108+
expect(screen.queryByText(/pre-GP2/i)).toBeNull();
109+
expect(
110+
screen.queryByText(/no public UF2 download or installer action/i),
111+
).toBeNull();
112+
113+
// Accurate five-target provisioning summary instead.
114+
const methods = screen
115+
.getByRole("heading", { name: /two provisioning methods/i })
116+
.closest("section");
117+
expect(methods).toBeDefined();
118+
expect(methods).toHaveTextContent(
119+
/ESP Web Tools.*Web Serial.*four exact ESP profiles/i,
120+
);
121+
expect(methods).toHaveTextContent(
122+
/esp32-4mb.*esp32-s3-n16r8.*waveshare-esp32-s3-lcd-147b.*esp32-c3-4mb/i,
123+
);
124+
expect(methods).toHaveTextContent(/Pico 2 W.*BOOTSEL.*UF2/i);
125+
126+
// The profile explainer covers all five profiles with exact requirements.
127+
const profileSection = screen
128+
.getByRole("heading", { name: /choose the exact module profile/i })
129+
.closest("section");
130+
expect(profileSection).toBeDefined();
131+
const classicProfile = within(profileSection as HTMLElement)
132+
.getByText("esp32-4mb")
133+
.closest("li");
134+
expect(classicProfile).toHaveTextContent(/Classic ESP32 module/);
135+
const c3Profile = within(profileSection as HTMLElement)
136+
.getByText("esp32-c3-4mb")
137+
.closest("li");
138+
expect(c3Profile).toHaveTextContent(/revision v0\.3 or newer.*4 MiB flash/i);
139+
const waveshareProfile = within(profileSection as HTMLElement)
140+
.getByText("waveshare-esp32-s3-lcd-147b")
141+
.closest("li");
142+
expect(waveshareProfile).toHaveTextContent(
143+
/B version.*16 MiB flash.*8 MiB Octal PSRAM/i,
144+
);
145+
const picoProfile = within(profileSection as HTMLElement)
146+
.getByText("rpi-pico2-w")
147+
.closest("li");
148+
expect(picoProfile).toHaveTextContent(/RP2350.*CYW43439/i);
149+
expect(picoProfile).toHaveTextContent(/BOOTSEL/i);
150+
expect(picoProfile).toHaveTextContent(/not a Web Serial flow/i);
151+
152+
// "Before you connect" must not claim Web Serial or desktop Chromium as a
153+
// requirement of the Pico flow.
154+
const beforeConnect = screen
155+
.getByRole("heading", { name: /before you connect/i })
156+
.closest("section");
157+
expect(beforeConnect).toBeDefined();
158+
expect(beforeConnect).toHaveTextContent(
159+
/ESP.*Web Serial.*desktop Chromium|desktop Chromium.*Web Serial/i,
160+
);
161+
expect(beforeConnect).toHaveTextContent(/Pico 2 W does not use Web Serial/i);
162+
expect(beforeConnect).toHaveTextContent(/secure context.*Web Crypto/i);
163+
expect(beforeConnect).not.toHaveTextContent(
164+
/The installer uses Web Serial and Web Crypto/i,
165+
);
166+
167+
// The visible recovery section carries all four ESP merged-image commands.
168+
const recoverySection = screen
169+
.getByRole("heading", { name: /recover an interrupted flash/i })
170+
.closest("section");
171+
expect(recoverySection).toBeDefined();
172+
const recoveryCommands = [
173+
"python -m esptool --chip esp32 write_flash 0x1000 esp32-4mb/firmware.bin",
174+
"python -m esptool --chip esp32s3 write_flash 0x0 esp32-s3-n16r8/firmware.bin",
175+
"python -m esptool --chip esp32s3 write_flash 0x0 waveshare-esp32-s3-lcd-147b/firmware.bin",
176+
"python -m esptool --chip esp32c3 write_flash 0x0 esp32-c3-4mb/firmware.bin",
177+
];
178+
for (const command of recoveryCommands) {
179+
expect(
180+
screen.getByText(
181+
(_content, element) =>
182+
element?.tagName === "CODE" &&
183+
element.textContent?.replace(/\s+/g, " ").trim() === command,
184+
),
185+
).toBeInTheDocument();
186+
}
187+
188+
// Component offsets name the C3 beside the S3.
189+
expect(recoverySection).toHaveTextContent(
190+
/bootloader at 0x1000 for classic ESP32 or 0x0 for ESP32-S3 and ESP32-C3/i,
191+
);
192+
193+
// Separate Pico BOOTSEL re-copy recovery with no esptool/offset/Web Serial
194+
// fallback inside it.
195+
const picoRecovery = within(recoverySection as HTMLElement)
196+
.getByText(/Pico 2 W recovery/i)
197+
.closest("p");
198+
expect(picoRecovery).toBeDefined();
199+
expect(picoRecovery).toHaveTextContent(
200+
/hold BOOTSEL while reconnecting USB/i,
201+
);
202+
expect(picoRecovery).toHaveTextContent(/RP2350 volume/i);
203+
expect(picoRecovery).toHaveTextContent(/same versioned UF2/i);
204+
expect(picoRecovery).toHaveTextContent(/automatic reboot/i);
205+
expect(picoRecovery).toHaveTextContent(/PyBLE-XXXX/i);
206+
expect(picoRecovery).toHaveTextContent(
207+
/does not use Web Serial or the ESP ROM recovery controls/i,
208+
);
209+
expect(picoRecovery?.textContent).not.toMatch(/esptool|write_flash|0x[0-9]/i);
210+
}
211+
51212
function localPreviewArtifact(
52213
path: string,
53214
digestCharacter: string,
@@ -217,6 +378,44 @@ describe("firmware installer release copy", () => {
217378
}
218379
});
219380

381+
it("describes the staged five-profile candidate accurately with pending-qualification framing", async () => {
382+
await withSelectedRelease(fiveProfileV060Release("candidate"), () => {
383+
render(<FlashPage />);
384+
385+
expectFiveProfileFlashPageCopy();
386+
387+
// Candidate mode alone carries the visibly pending candidate framing.
388+
expect(
389+
screen.getByText(/access-controlled.*release candidate/i),
390+
).toBeInTheDocument();
391+
const methods = screen
392+
.getByRole("heading", { name: /two provisioning methods/i })
393+
.closest("section");
394+
expect(methods).toHaveTextContent(/visibly pending candidate/i);
395+
expect(methods).toHaveTextContent(/hardware qualification.*pending/i);
396+
});
397+
});
398+
399+
it("covers all five targets in the qualified v0.6.0 release copy", async () => {
400+
await withSelectedRelease(fiveProfileV060Release("public"), () => {
401+
render(<FlashPage />);
402+
403+
expect(
404+
screen.getByText(
405+
/qualified v0\.6\.0 firmware is available for all five exact release profiles/i,
406+
),
407+
).toBeInTheDocument();
408+
409+
expectFiveProfileFlashPageCopy();
410+
411+
// Qualified-public mode never claims a pending candidate.
412+
const methods = screen
413+
.getByRole("heading", { name: /two provisioning methods/i })
414+
.closest("section");
415+
expect(methods).not.toHaveTextContent(/visibly pending candidate/i);
416+
});
417+
});
418+
220419
it("keeps the historical public beta available without a current exact-board claim", async () => {
221420
const selectionRoot = await mkdtemp(
222421
join(tmpdir(), "pyble-flash-qualified-selection-"),
@@ -328,6 +527,8 @@ describe("firmware installer release copy", () => {
328527
expect(boardSection).toHaveTextContent(/172.*320.*ST7789V3/i);
329528
expect(boardSection).toHaveTextContent(/pyble_st7789/i);
330529
expect(boardSection).toHaveTextContent(/pyble_waveshare_lcd147b/i);
530+
expect(boardSection).toHaveTextContent(/Blockly TFT programs/);
531+
expect(boardSection).not.toHaveTextContent(/Blocky TFT/);
331532
expect(boardSection).toHaveTextContent(
332533
/fresh erased installation.*boot splash.*shown by default.*persistently disable/i,
333534
);

‎tools/web/src/test/site-contract.test.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1036,11 +1036,14 @@ describe("public-site contract", () => {
10361036
const deferredC3 = within(plannedProfiles as HTMLElement)
10371037
.getByText("esp32-c3-4mb")
10381038
.closest("li");
1039-
expect(deferredC3).toHaveAttribute("aria-disabled", "true");
1039+
// Informational list items are not interactive; aria-disabled is ARIA
1040+
// misuse there.
1041+
expect(deferredC3).not.toHaveAttribute("aria-disabled");
10401042
expect(deferredC3).toHaveTextContent(/unavailable/i);
10411043
expect(deferredC3).toHaveTextContent(
10421044
/exact-profile real-hardware validation/i,
10431045
);
1046+
expect(plannedProfiles).not.toHaveTextContent(/pre-GP2/i);
10441047
expect(
10451048
screen.getByRole("heading", { name: /recover an interrupted flash/i }),
10461049
).toBeInTheDocument();

0 commit comments

Comments
 (0)