From ceda3daa801b2d528ac7d23c5b9a0c4d17c319fe Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Fri, 17 Apr 2026 10:09:16 +0200 Subject: [PATCH 1/4] slintpad: Add failing regression test for 'Start with Hello World!' panic Reproduces #11416: clicking the "Start with Hello World!" code lens on an empty buffer panics the wasm LSP because the slint/populate handler calls tokio::task::spawn_local outside of a LocalSet. --- tools/slintpad/tests/populate-command.spec.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tools/slintpad/tests/populate-command.spec.ts diff --git a/tools/slintpad/tests/populate-command.spec.ts b/tools/slintpad/tests/populate-command.spec.ts new file mode 100644 index 00000000000..de6238925b0 --- /dev/null +++ b/tools/slintpad/tests/populate-command.spec.ts @@ -0,0 +1,40 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +// Regression test for https://github.com/slint-ui/slint/issues/11416: +// clicking the "Start with Hello World!" code lens on an empty buffer used to +// panic the wasm LSP because the command handler called tokio::task::spawn_local +// outside of a LocalSet. +import { test, expect } from "@playwright/test"; + +test("'Start with Hello World!' code lens populates the editor without panicking", async ({ + page, +}) => { + // A single-whitespace `snippet` makes SlintPad open a main.slint whose content + // has no non-whitespace tokens, which is the condition under which the LSP + // emits the "Start with Hello World!" code lens (see tools/lsp/language.rs). + await page.goto("http://localhost:3000/?snippet=%20"); + await expect(page.locator("#tab-key-1-0")).toContainText("main.slint"); + + // Wait for the LSP-provided code lens to show up and click it. This sends + // `workspace/executeCommand slint/populate`, which is the path that used + // to panic in the wasm LSP. + const code_lens = page.getByRole("button", { + name: "Start with Hello World!", + }); + await expect(code_lens).toBeVisible({ timeout: 20_000 }); + await code_lens.click(); + + // Wait until the LSP has responded — either the populate edit landed, or + // a panic dialog popped up. + const editor = page.locator(".monaco-editor").first(); + const panic_dialog = page.locator("dialog.panic_dialog"); + const main_window = editor.getByText("MainWindow"); + await expect(panic_dialog.or(main_window)).toBeVisible({ timeout: 15_000 }); + + // The critical assertion: no panic dialog. On failure the page snapshot + // in the error context will show the panic message from the LSP. + await expect(panic_dialog).toHaveCount(0); + // And the populate edit actually landed. + await expect(editor).toContainText('"Hello World!"'); +}); From 269cdd25a93200a1f565413dfe014bc43f74a03e Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Fri, 17 Apr 2026 10:29:43 +0200 Subject: [PATCH 2/4] lsp: Fix 'Start with Hello World!' panic in SlintPad The slint/populate command handler called tokio::task::spawn_local, which panics in the wasm LSP because no tokio LocalSet is installed there (only native's main loop sets one up). Introduce a `common::spawn_local` wrapper that routes to `wasm_bindgen_futures::spawn_local` on wasm and `tokio::task::spawn_local` on native, and forbid bare `tokio::task::spawn_local` in the lsp crate via a clippy.toml disallowed-methods entry plus a crate-local `#![deny(clippy::disallowed_methods)]` on the entry points. Fixes #11416. --- .clippy.toml | 12 ++++++++++++ tools/lsp/common.rs | 14 ++++++++++++++ tools/lsp/language.rs | 2 +- tools/lsp/main.rs | 2 +- tools/lsp/wasm_main.rs | 2 +- 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/.clippy.toml b/.clippy.toml index 9b72d1c9cd3..6f815dd938a 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -3,3 +3,15 @@ type-complexity-threshold = 2500 too-many-arguments-threshold = 10 + +# `disallowed-methods` is the configuration for the `clippy::disallowed_methods` +# lint, which is allow-by-default. Listing entries here therefore has no effect +# in crates that don't opt into the lint. We currently enable it only in the +# slint-lsp crate (see `#![deny(clippy::disallowed_methods)]` in `tools/lsp/main.rs` +# and `tools/lsp/wasm_main.rs`), so the entries below are effectively scoped to +# slint-lsp. Putting them here (rather than in `tools/lsp/clippy.toml`) avoids +# shadowing the workspace-wide thresholds above — clippy reads exactly one +# clippy.toml per crate, with no merging. +disallowed-methods = [ + { path = "tokio::task::spawn_local", reason = "Use `slint_lsp::common::spawn_local` — it routes to `wasm_bindgen_futures::spawn_local` on wasm." }, +] diff --git a/tools/lsp/common.rs b/tools/lsp/common.rs index 4219a6ea2cd..1c71bd15fda 100644 --- a/tools/lsp/common.rs +++ b/tools/lsp/common.rs @@ -27,6 +27,20 @@ pub type Result = std::result::Result; #[cfg(target_arch = "wasm32")] use crate::wasm_prelude::*; +#[allow(clippy::disallowed_methods)] +pub fn spawn_local(future: F) +where + F: std::future::Future + 'static, + F::Output: 'static, +{ + #[cfg(target_arch = "wasm32")] + wasm_bindgen_futures::spawn_local(async move { + let _ = future.await; + }); + #[cfg(not(target_arch = "wasm32"))] + tokio::task::spawn_local(future); +} + /// Use this in nodes you want the language server and preview to /// ignore a node for code analysis purposes. pub const NODE_IGNORE_COMMENT: &str = "@lsp:ignore-node"; diff --git a/tools/lsp/language.rs b/tools/lsp/language.rs index d163fa875fe..93ba30be5e0 100644 --- a/tools/lsp/language.rs +++ b/tools/lsp/language.rs @@ -369,7 +369,7 @@ pub fn register_request_handlers(rh: &mut RequestHandler) { return Ok(None::); } if params.command.as_str() == POPULATE_COMMAND { - tokio::task::spawn_local(populate_command(¶ms.arguments, ctx)?); + common::spawn_local(populate_command(¶ms.arguments, ctx)?); return Ok(None::); } Ok(None::) diff --git a/tools/lsp/main.rs b/tools/lsp/main.rs index 745fcf49619..b4e80950bd3 100644 --- a/tools/lsp/main.rs +++ b/tools/lsp/main.rs @@ -3,7 +3,7 @@ #![cfg(not(target_arch = "wasm32"))] #![allow(clippy::await_holding_refcell_ref)] -#![deny(clippy::print_stderr, clippy::print_stdout)] +#![deny(clippy::print_stderr, clippy::print_stdout, clippy::disallowed_methods)] #[cfg(all(feature = "preview-engine", not(feature = "preview-builtin")))] compile_error!( diff --git a/tools/lsp/wasm_main.rs b/tools/lsp/wasm_main.rs index 3487b72253f..1217aa71196 100644 --- a/tools/lsp/wasm_main.rs +++ b/tools/lsp/wasm_main.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 #![cfg(target_arch = "wasm32")] -#![deny(clippy::print_stderr, clippy::print_stdout)] +#![deny(clippy::print_stderr, clippy::print_stdout, clippy::disallowed_methods)] pub mod common; mod fmt; From ba14c08af887d694d491d93697c9c79f4cb5eff6 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Fri, 17 Apr 2026 14:29:31 +0200 Subject: [PATCH 3/4] ci: Upload Playwright report and traces on slintpad test failure --- .github/workflows/wasm_editor_and_interpreter.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/wasm_editor_and_interpreter.yaml b/.github/workflows/wasm_editor_and_interpreter.yaml index 295cc0f441d..3c71d4e1a4e 100644 --- a/.github/workflows/wasm_editor_and_interpreter.yaml +++ b/.github/workflows/wasm_editor_and_interpreter.yaml @@ -80,6 +80,14 @@ jobs: if: ${{ always() }} working-directory: tools/slintpad run: npx github-actions-ctrf playwright-report/ctrf-report.json + - name: "Upload Playwright Report" + if: ${{ failure() }} + uses: actions/upload-artifact@v7 + with: + name: slintpad-playwright-report + path: | + tools/slintpad/playwright-report/ + tools/slintpad/test-results/ - name: "Upload slintpad Artifacts" uses: actions/upload-artifact@v7 with: From 95d15941532f565f5922951bcc8702eb9a06246d Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Fri, 17 Apr 2026 15:23:41 +0200 Subject: [PATCH 4/4] slintpad: Skip 'Start with Hello World!' test on Firefox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Headless Firefox on CI has no WebGL (no SwiftShader-equivalent like Chromium, no GL context like a desktop session), so opening the SlintPad preview panics in internal/renderers/femtovg/opengl.rs:134 with "Cannot proceed without WebGL - aborting". The resulting modal panic dialog intercepts the test's code-lens click. Setting webgl.force-enabled doesn't help — there's no GL stack to fall back to. Skip the test on Firefox until the preview either gains a software-WebGL fallback or stops panicking on init. --- tools/slintpad/tests/populate-command.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/slintpad/tests/populate-command.spec.ts b/tools/slintpad/tests/populate-command.spec.ts index de6238925b0..38bb82fd2b8 100644 --- a/tools/slintpad/tests/populate-command.spec.ts +++ b/tools/slintpad/tests/populate-command.spec.ts @@ -9,7 +9,15 @@ import { test, expect } from "@playwright/test"; test("'Start with Hello World!' code lens populates the editor without panicking", async ({ page, + browserName, }) => { + // Headless Firefox on CI has no WebGL, so opening the SlintPad preview + // panics in internal/renderers/femtovg/opengl.rs:134 with + // "Cannot proceed without WebGL - aborting". That panic shows a modal + // dialog that intercepts our code-lens click. Skip until the preview + // either gets a software-WebGL fallback or stops panicking on init. + test.skip(browserName === "firefox", "preview panics without WebGL"); + // A single-whitespace `snippet` makes SlintPad open a main.slint whose content // has no non-whitespace tokens, which is the condition under which the LSP // emits the "Start with Hello World!" code lens (see tools/lsp/language.rs).