diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index ed56678..5398220 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -31,10 +31,61 @@ body: validations: required: true - type: input - id: hardware + id: profile attributes: - label: Hardware and platform - description: Exact board/profile and iPadOS, Android, browser, or desktop version + label: Exact installer profile ID + description: For installer problems, use the exact profile ID shown on /flash; otherwise enter "Not installer-related." + validations: + required: true + - type: input + id: module + attributes: + label: Exact board model and module marking + description: Copy the board model and the text printed on the module; enter "Not applicable" only when no board is involved. + validations: + required: true + - type: input + id: memory + attributes: + label: Memory configuration + description: Flash capacity, PSRAM capacity, and PSRAM type (Octal, Quad, none, or unknown). + validations: + required: true + - type: input + id: browser + attributes: + label: Browser + description: Browser name and exact version; enter "Not applicable" when the browser is not involved. + validations: + required: true + - type: input + id: operating_system + attributes: + label: Desktop operating system + description: Desktop operating system and exact version; enter "Not applicable" when the installer is not involved. + validations: + required: true + - type: input + id: installer_stage + attributes: + label: Failed installer stage + description: For example page load, browser check, profile selection, artifact verification, port selection, erase, write, verification, first boot, or recovery; otherwise enter "Not installer-related." + validations: + required: true + - type: input + id: tablet + attributes: + label: Exact tablet or device model + description: Name the iPad or Android device used with the app; enter "Not applicable" only when the app is not involved. + validations: + required: true + - type: input + id: tablet_os + attributes: + label: Tablet operating system + description: Tablet operating system and exact version (iPadOS or Android); enter "Not applicable" only when the app is not involved. + validations: + required: true - type: textarea id: steps attributes: diff --git a/README.md b/README.md index ffea44a..d508b3c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ compatible microcontroller board over Bluetooth Low Energy. Its normal workflow needs no USB serial connection, Wi-Fi onboarding, cloud account, or telemetry. -- Website and browser installer: [pyble.dev](https://pyble.dev) +- Website and browser-installer status: [pyble.dev](https://pyble.dev) - iPad external beta: [join with TestFlight](https://testflight.apple.com/join/yU4e8s6d) - License: [MIT](LICENSE) @@ -27,7 +27,7 @@ telemetry.

- Actual PyBLE app: a GPIO 48 NeoPixel Blocks program and its generated MicroPython on the pictured ESP32-S3 board. + Actual PyBLE app in landscape: GPIO 48 NeoPixel Blocks beside the generated MicroPython.

## What works @@ -62,16 +62,17 @@ BLE GATT peripheral. It supports: - board naming and identify support; and - upstream MicroPython’s standard `neopixel` module. -The public browser installer currently offers qualified images for: +The public browser installer is currently unavailable pending v0.4.2 HIL on +both exact current profiles: -| Installer profile | Typical target | Availability | -| ----------------- | ---------------------------------- | ------------------------------------------- | -| `esp32-4mb` | Classic ESP32, 4 MB flash | Available | -| `esp32-s3-n16r8` | ESP32-S3, 16 MB flash / 8 MB PSRAM | Available | -| `esp32-c3-4mb` | ESP32-C3, 4 MB flash | Source target; public installer pending HIL | +| Installer profile | Exact target constraint | Public status | +| ----------------- | ------------------------------------------------------------ | --------------------------------------------------- | +| `esp32-4mb` | Classic ESP32, 4 MiB external SPI flash; no PSRAM assumed | v0.4.2 HIL pending; installer unavailable | +| `esp32-s3-n16r8` | ESP32-S3, 16 MiB flash / 8 MiB Octal PSRAM; N16R8-class only | v0.4.2 HIL pending; installer unavailable | +| `esp32-c3-4mb` | ESP32-C3, 4 MiB external SPI flash; no PSRAM assumed | Planned; no public image; exact-profile HIL pending | -These are the initial validated ports, not a chip-family allowlist. A future -board is compatible when it has a maintained PyBLE agent port, BLE GATT +These are the initial port targets, not a chip-family allowlist. A future board +is compatible when it has a maintained PyBLE agent port, BLE GATT peripheral support, adequate resources, PBLE/1 conformance, recovery testing, and hardware-validation evidence. Stock MicroPython plus generic Bluetooth hardware is not sufficient by itself. @@ -102,14 +103,17 @@ shared conformance corpus, documentation, and CI atomically. ## Try PyBLE -1. Open [pyble.dev/flash](https://pyble.dev/flash) in desktop Chrome or Edge. -2. Select the exact supported profile for your board and flash the qualified - agent firmware. Flashing erases the board; review the installer warning and - back up files first. -3. Install the iPad beta from +1. Install the iPad beta from [TestFlight](https://testflight.apple.com/join/yU4e8s6d), or build the Flutter app locally. -4. Open PyBLE, scan for the board, connect, and run an example. +2. Check [pyble.dev/flash](https://pyble.dev/flash) in desktop Chrome or Edge. + The public installer is currently unavailable pending v0.4.2 HIL. Wait for + that page to show an active release version, your exact profile, and an + enabled install action. +3. Only after that gate opens, back up the board, confirm its exact memory + profile, and use the one-time wired installer. Flashing erases the board. +4. Open PyBLE, scan for the provisioned board, connect, and run an example over + BLE. See [support and troubleshooting](https://pyble.dev/support) for browser, Bluetooth, and recovery requirements. diff --git a/docs/specifications/website.md b/docs/specifications/website.md index 23dc0fb..802ed3f 100644 --- a/docs/specifications/website.md +++ b/docs/specifications/website.md @@ -132,6 +132,40 @@ platform cropping can make it unreliable. A separate local square TestFlight card MAY include the exact invitation QR, visible destination, and plain-text instructions. +### 3.4 Pre-activation release claims and support intake + +Until the first v0.4.2 public firmware selector passes the complete gate in §7, +the repository README and home page MUST say that the browser installer is +unavailable pending v0.4.2 HIL on both exact current profiles. They MUST NOT +describe either profile or its browser image as released, available, or +qualified. The home-page target cards MUST identify the profiles and constraints +as `esp32-4mb` (classic ESP32, 4 MiB external SPI flash, no PSRAM assumed) and +`esp32-s3-n16r8` (ESP32-S3, 16 MiB flash, 8 MiB **Octal** PSRAM), and give each +the truthful pending-HIL/unavailable state. `esp32-c3-4mb` remains a separate +planned, unavailable profile. + +README getting-started instructions MUST gate destructive flashing on `/flash` +showing an active version, exact profile, and enabled install action. While the +installer is unavailable, they MUST NOT instruct a reader to select or flash a +supposedly qualified public image. A real-app capture caption MUST describe only +what is visible; it MUST NOT claim that a physical board is pictured when the +capture shows only the app. + +While the installer is unavailable, the wide social card MUST describe the +workflow as one-time USB setup followed by everyday use over BLE. It MUST NOT +advertise an enabled web flasher; it MAY explicitly label firmware HIL as +pending. Its mechanically rendered PNG and authored SVG MUST remain paired by +reviewed content and exact-dimension tests. + +The support route MUST link directly to the preferred GitHub bug template at +`https://github.com/PyBLE-dev/PyBLE/issues/new?template=bug.yml`. Installer +intake MUST request the exact profile ID, board/model and module marking, flash +capacity, PSRAM capacity and type, browser name/version, desktop operating +system/version, failed installer stage, redacted error text, exact tablet/device +model, and tablet operating system/version (iPadOS or Android). The issue +template and support-page checklist MUST agree on those fields and remind users +to remove secrets and personal identifiers. + ## 4. Brand and visual contract The canonical prompt-chip SVG in `app/assets/branding/` is the source asset. A diff --git a/tests/publication/test_public_claims.py b/tests/publication/test_public_claims.py new file mode 100644 index 0000000..e2b0529 --- /dev/null +++ b/tests/publication/test_public_claims.py @@ -0,0 +1,88 @@ +# SPDX-License-Identifier: MIT +# Part of PyBLE (https://pyble.dev) — see /LICENSE. + +from __future__ import annotations + +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def markdown_section(document: str, heading: str) -> str: + marker = f"## {heading}\n" + start = document.index(marker) + len(marker) + end = document.find("\n## ", start) + return document[start:] if end < 0 else document[start:end] + + +class PublicClaimsTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8") + cls.bug_template = ( + REPO_ROOT / ".github" / "ISSUE_TEMPLATE" / "bug.yml" + ).read_text(encoding="utf-8") + + def test_readme_is_truthful_before_v042_hil_completes(self) -> None: + firmware = markdown_section(self.readme, "What works") + + self.assertIn( + "public browser installer is currently unavailable pending v0.4.2 HIL", + firmware, + ) + self.assertIn("`esp32-4mb`", firmware) + self.assertIn("Classic ESP32, 4 MiB external SPI flash", firmware) + self.assertIn("`esp32-s3-n16r8`", firmware) + self.assertIn("16 MiB flash / 8 MiB Octal PSRAM", firmware) + self.assertNotIn("currently offers qualified images", firmware) + self.assertNotIn("| Available", firmware) + + def test_readme_caption_describes_only_the_visible_app(self) -> None: + caption_start = self.readme.index("Actual PyBLE app") + caption_end = self.readme.index("", caption_start) + caption = self.readme[caption_start:caption_end] + + self.assertIn("in landscape", caption) + self.assertIn("GPIO 48 NeoPixel Blocks", caption) + self.assertIn("generated MicroPython", caption) + self.assertNotRegex(caption, r"(?i)pictured|board|module") + + def test_readme_try_steps_are_gated_on_an_active_installer(self) -> None: + try_section = markdown_section(self.readme, "Try PyBLE") + + self.assertIn("currently unavailable pending v0.4.2 HIL", try_section) + self.assertIn("active release version", try_section) + self.assertIn("enabled install action", try_section) + self.assertNotRegex(try_section, r"(?is)select .*qualified\s+agent firmware") + + def test_bug_template_collects_the_exact_installer_diagnostics(self) -> None: + for field_id in ( + "profile", + "module", + "memory", + "browser", + "operating_system", + "installer_stage", + "tablet", + "tablet_os", + ): + self.assertIn(f" id: {field_id}\n", self.bug_template) + + for wording in ( + "Exact installer profile ID", + "Exact board model and module marking", + "Flash capacity, PSRAM capacity, and PSRAM type", + "Browser name and exact version", + "Desktop operating system and exact version", + "Failed installer stage", + "Exact tablet or device model", + "Tablet operating system and exact version", + ): + self.assertIn(wording, self.bug_template) + + self.assertRegex(self.bug_template, r"(?i)remove.*(?:secret|credential)") + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/web/public/social/pyble-beta-og-1200x630.png b/tools/web/public/social/pyble-beta-og-1200x630.png index e801659..875f61d 100644 Binary files a/tools/web/public/social/pyble-beta-og-1200x630.png and b/tools/web/public/social/pyble-beta-og-1200x630.png differ diff --git a/tools/web/public/social/pyble-beta-og-1200x630.svg b/tools/web/public/social/pyble-beta-og-1200x630.svg index 23c229e..09f6d03 100644 --- a/tools/web/public/social/pyble-beta-og-1200x630.svg +++ b/tools/web/public/social/pyble-beta-og-1200x630.svg @@ -11,8 +11,9 @@ > PyBLE public beta - MicroPython from your iPad over Bluetooth Low Energy, with a real PyBLE - Blocks workspace and generated Python code. + PyBLE iPad beta with one-time USB firmware setup followed by everyday + MicroPython development over Bluetooth Low Energy, shown with a real Blocks + workspace and generated Python code. @@ -101,8 +102,8 @@ font-size="20" font-weight="450" > - Edit, run, and learn with Python or Blocks— - without reaching for a cable. + One-time USB setup. + Everyday coding over BLE. @@ -110,8 +111,8 @@ TESTFLIGHT BETA - - WEB FLASHER + + FIRMWARE HIL PENDING div { display: flex; min-width: 0; - min-height: 190px; + min-height: 275px; padding: 25px 17px; align-items: center; justify-content: flex-end; @@ -1062,7 +1062,8 @@ h3 { .target-grid strong { color: var(--white); - font-size: 0.95rem; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-size: 0.78rem; } .target-grid small { @@ -1070,11 +1071,30 @@ h3 { font-size: 0.68rem; } +.target-grid__target-name { + margin-top: 6px; + color: #dceaff; + font-size: 0.78rem; + font-weight: 700; +} + +.target-grid__constraint { + margin-top: 10px; + line-height: 1.45; +} + +.target-grid .target-grid__status { + margin-top: 9px; + color: var(--warning); + font-weight: 700; + line-height: 1.4; +} + .target-grid > .target-grid__target--planned { border-style: dashed; } -.target-grid__target--planned small { +.target-grid__target--planned .target-grid__status { color: var(--warning); } diff --git a/tools/web/src/app/page.tsx b/tools/web/src/app/page.tsx index 5c5d8a2..0fce271 100644 --- a/tools/web/src/app/page.tsx +++ b/tools/web/src/app/page.tsx @@ -57,7 +57,7 @@ const steps = [ { number: "01", title: "Provision once", - body: "Install matching PyBLE-enabled MicroPython firmware once. Initial beta images are available for compatible ESP32 targets.", + body: "After v0.4.2 passes HIL, use USB once to install the exact matching PyBLE-enabled MicroPython profile. The public installer is unavailable during qualification.", }, { number: "02", @@ -87,10 +87,11 @@ export default function HomePage() {

Code your MicroPython board. Leave the cable behind.

PyBLE is a free, tablet-first IDE designed for boards that run - MicroPython and support Bluetooth Low Energy. The current firmware - release supports qualified classic ESP32 and ESP32-S3 module - profiles. ESP32-C3 and more microcontroller families remain - planned. + MicroPython and support Bluetooth Low Energy. Public v0.4.2 + firmware is pending HIL for the exact esp32-4mb and esp32-s3-n16r8 + profiles. The public browser installer stays unavailable until + both exact profiles pass HIL. ESP32-C3 and more microcontroller + families remain planned.

@@ -248,11 +249,11 @@ export default function HomePage() { {initialFirmwareTargets.map((target) => (
- {target.name} - {target.availability} + {target.id} + + {target.target} + + + {target.constraint} + + {target.status}
))}
@@ -339,9 +346,10 @@ export default function HomePage() {

External testing is open

Join the PyBLE beta on TestFlight.

- Install the free iPad beta through Apple TestFlight. Then use - desktop Chrome to provision a supported board and start coding - over Bluetooth Low Energy. + Install the free iPad beta through Apple TestFlight now. Public + board provisioning will open only after v0.4.2 passes HIL on both + exact release profiles; after that one-time USB setup, everyday + coding runs over Bluetooth Low Energy.

- Install board firmware + Check firmware status
diff --git a/tools/web/src/app/support/page.tsx b/tools/web/src/app/support/page.tsx index 0812d90..9af5fbf 100644 --- a/tools/web/src/app/support/page.tsx +++ b/tools/web/src/app/support/page.tsx @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Part of PyBLE (https://pyble.dev) — see /LICENSE. -import { MailIcon } from "@/components/icons"; +import { ExternalIcon, MailIcon } from "@/components/icons"; import { PageIntro } from "@/components/page-intro"; import { pageMetadata, siteConfig } from "@/lib/site"; @@ -13,11 +13,15 @@ export const metadata = pageMetadata({ }); const diagnosticItems = [ - "Board and chip family", + "Exact installer profile ID (or “not installer-related”)", + "Exact board model and module marking", + "Flash capacity, PSRAM capacity, and PSRAM type", + "Browser name/version and desktop OS name/version", + "Failed installer stage and redacted error text", + "Exact tablet or device model", + "Tablet OS: iPadOS or Android name/version", "PyBLE app and agent versions", - "iPadOS or Android version", "The exact steps that caused the problem", - "Relevant console text and a screenshot", ] as const; export default function SupportPage() { @@ -38,12 +42,12 @@ export default function SupportPage() {
  • 1
    -

    Install the matching firmware once

    +

    Check firmware status before installing

    - The public installer supports only esp32-4mb and - esp32-s3-n16r8. ESP32-C3 is not currently available. Use the - reviewed image supplied for your exact target; this initial - step uses a cable. + The public installer is unavailable while v0.4.2 HIL runs + for esp32-4mb and esp32-s3-n16r8. ESP32-C3 is not currently + available. Wait for the installer status to show an active + release and enabled action; this initial step uses a cable.

  • @@ -139,7 +143,7 @@ export default function SupportPage() { diff --git a/tools/web/src/lib/site.ts b/tools/web/src/lib/site.ts index 554a9ef..7425d18 100644 --- a/tools/web/src/lib/site.ts +++ b/tools/web/src/lib/site.ts @@ -13,6 +13,8 @@ export const siteConfig = { supportEmail: "viwat.v@chula.ac.th", testFlightUrl: "https://testflight.apple.com/join/yU4e8s6d", repositoryUrl: "https://github.com/PyBLE-dev/PyBLE", + bugReportUrl: + "https://github.com/PyBLE-dev/PyBLE/issues/new?template=bug.yml", } as const; export const navigation = [ @@ -23,9 +25,27 @@ export const navigation = [ ] as const; export const initialFirmwareTargets = [ - { name: "ESP32", availability: "Released profile" }, - { name: "ESP32-S3", availability: "Released profile" }, - { name: "ESP32-C3", availability: "Planned profile" }, + { + id: "esp32-4mb", + target: "Classic ESP32", + constraint: "4 MiB external SPI flash · no PSRAM assumed", + status: "v0.4.2 HIL pending · installer unavailable", + planned: false, + }, + { + id: "esp32-s3-n16r8", + target: "ESP32-S3 N16R8", + constraint: "16 MiB flash · 8 MiB Octal PSRAM", + status: "v0.4.2 HIL pending · installer unavailable", + planned: false, + }, + { + id: "esp32-c3-4mb", + target: "ESP32-C3", + constraint: "4 MiB external SPI flash · no PSRAM assumed", + status: "Planned · installer unavailable pending exact-profile HIL", + planned: true, + }, ] as const; export function absoluteUrl(path: string): string { diff --git a/tools/web/src/test/site-contract.test.tsx b/tools/web/src/test/site-contract.test.tsx index 2c994ff..03a06c3 100644 --- a/tools/web/src/test/site-contract.test.tsx +++ b/tools/web/src/test/site-contract.test.tsx @@ -99,8 +99,12 @@ describe("public-site contract", () => { }); const publicDirectory = join(process.cwd(), "public"); - const [socialPng, qrCardPng, qrCardSvg] = await Promise.all([ + const [socialPng, socialSvg, qrCardPng, qrCardSvg] = await Promise.all([ readFile(join(publicDirectory, "social", "pyble-beta-og-1200x630.png")), + readFile( + join(publicDirectory, "social", "pyble-beta-og-1200x630.svg"), + "utf8", + ), readFile(join(publicDirectory, "social", "pyble-testflight-qr-1080.png")), readFile( join(publicDirectory, "social", "pyble-testflight-qr-1080.svg"), @@ -110,6 +114,13 @@ describe("public-site contract", () => { expect([socialPng.readUInt32BE(16), socialPng.readUInt32BE(20)]).toEqual([ 1200, 630, ]); + expect(socialSvg).toContain("One-time USB setup."); + expect(socialSvg).toContain("Everyday coding over BLE."); + expect(socialSvg).toContain("FIRMWARE HIL PENDING"); + expect(socialSvg).not.toContain("WEB FLASHER"); + expect(createHash("sha256").update(socialPng).digest("hex")).toBe( + "7e7e037d9bd2e58e2e66f516bfd6f1b753bda472c402b8130f8a8a6dd8f19ba9", + ); expect([qrCardPng.readUInt32BE(16), qrCardPng.readUInt32BE(20)]).toEqual([ 1080, 1080, ]); @@ -138,7 +149,7 @@ describe("public-site contract", () => { ); }); - it("states the vendor-neutral vision and separates it from initial firmware support", () => { + it("states the vendor-neutral vision and the truthful pre-activation firmware state", () => { render(); expect( @@ -152,11 +163,12 @@ describe("public-site contract", () => { /designed for boards that run MicroPython and support Bluetooth Low Energy/i, ), ).toBeInTheDocument(); - expect( - screen.getByText( - /current firmware release supports qualified classic ESP32 and ESP32-S3 module profiles/i, - ), - ).toBeInTheDocument(); + expect(screen.getByText(/public v0\.4\.2 firmware/i)).toHaveTextContent( + /pending HIL for the exact esp32-4mb and esp32-s3-n16r8 profiles/i, + ); + expect(screen.getByText(/public v0\.4\.2 firmware/i)).toHaveTextContent( + /public browser installer stays unavailable until both exact profiles pass HIL/i, + ); expect( screen.getByText( /ESP32-C3 and more microcontroller families remain planned/i, @@ -304,17 +316,39 @@ describe("public-site contract", () => { expect(capture.readUInt32BE(20)).toBe(2732); }); - it("distinguishes current firmware families from the planned ESP32-C3 target", () => { + it("names every exact profile, memory constraint, and qualification state", () => { expect(initialFirmwareTargets).toEqual([ - { name: "ESP32", availability: "Released profile" }, - { name: "ESP32-S3", availability: "Released profile" }, - { name: "ESP32-C3", availability: "Planned profile" }, + { + id: "esp32-4mb", + target: "Classic ESP32", + constraint: "4 MiB external SPI flash · no PSRAM assumed", + status: "v0.4.2 HIL pending · installer unavailable", + planned: false, + }, + { + id: "esp32-s3-n16r8", + target: "ESP32-S3 N16R8", + constraint: "16 MiB flash · 8 MiB Octal PSRAM", + status: "v0.4.2 HIL pending · installer unavailable", + planned: false, + }, + { + id: "esp32-c3-4mb", + target: "ESP32-C3", + constraint: "4 MiB external SPI flash · no PSRAM assumed", + status: "Planned · installer unavailable pending exact-profile HIL", + planned: true, + }, ]); render(); - const esp32C3Target = screen.getByText("ESP32-C3").closest("div"); - expect(esp32C3Target).toHaveTextContent("Planned profile"); + for (const target of initialFirmwareTargets) { + const targetCard = screen.getByText(target.id).closest("div"); + expect(targetCard).toHaveTextContent(target.target); + expect(targetCard).toHaveTextContent(target.constraint); + expect(targetCard).toHaveTextContent(target.status); + } }); it("keeps the public installer unavailable while explaining exact profiles, BLE use, and recovery", () => { @@ -472,16 +506,34 @@ describe("public-site contract", () => { expect(screen.getByText(/ordinary request data/i)).toBeInTheDocument(); }); - it("gives beta users a concrete support checklist and contact", () => { + it("gives beta users an exact installer intake and preferred bug template", () => { render(); expect( screen.getByRole("heading", { level: 1, name: "Support" }), ).toBeInTheDocument(); - expect(screen.getByText("Board and chip family")).toBeInTheDocument(); + expect(screen.getByText(/exact installer profile ID/i)).toBeInTheDocument(); + expect( + screen.getByText(/exact board model and module marking/i), + ).toBeInTheDocument(); + expect( + screen.getByText(/flash capacity, PSRAM capacity, and PSRAM type/i), + ).toBeInTheDocument(); + expect( + screen.getByText(/browser name\/version and desktop OS name\/version/i), + ).toBeInTheDocument(); + expect( + screen.getByText(/failed installer stage and redacted error text/i), + ).toBeInTheDocument(); + expect( + screen.getByText(/exact tablet or device model/i), + ).toBeInTheDocument(); + expect( + screen.getByText(/iPadOS or Android name\/version/i), + ).toBeInTheDocument(); expect( screen.getByText( - /public installer supports only esp32-4mb and esp32-s3-n16r8/i, + /public installer is unavailable while v0\.4\.2 HIL runs for esp32-4mb and esp32-s3-n16r8/i, ), ).toBeInTheDocument(); expect( @@ -495,8 +547,14 @@ describe("public-site contract", () => { expect( screen.getByText("PyBLE app and agent versions"), ).toBeInTheDocument(); - expect( - screen.getByRole("link", { name: "viwat.v@chula.ac.th" }), - ).toHaveAttribute("href", "mailto:viwat.v@chula.ac.th"); + expect(siteConfig.bugReportUrl).toBe( + "https://github.com/PyBLE-dev/PyBLE/issues/new?template=bug.yml", + ); + const preferredReport = screen.getByRole("link", { + name: "Open the GitHub bug template", + }); + expect(preferredReport).toHaveAttribute("href", siteConfig.bugReportUrl); + expect(preferredReport).toHaveAttribute("target", "_blank"); + expect(preferredReport).toHaveAttribute("rel", "noopener noreferrer"); }); });