A floated promise is invisible at review time — the call reads like an awaited one minus four characters — and the unhandled rejection it produces surfaces in a different test, in a different file, as a stack pointing at SDK internals. In #1947 two un-held `callTool` calls made `npm run ci` unpassable and took a full investigation to attribute; the fix was two lines. `@typescript-eslint/no-floating-promises` catches the class outright, so enable it at `error` everywhere. The rule is type-aware and `tseslint.configs.recommended` is not, so each scope's config gains a parser project. The four clients name their own tsconfig projects (both of them — `src` and `__tests__`/`server` live in different ones). The root scope has no tsconfig at all, so add `tsconfig.lint.json`: a lint-only project covering `core/**`, `test-servers/src/**`, and `vitest.shared.mts`, emitting nothing and gating nothing. It sets `moduleResolution: bundler` deliberately — under NodeNext `core/`'s extensionless imports fail to resolve, every import degrades to `any`, and the rule silently stops seeing promises at all. Measured surface, and the fixes: - core (2) — `stream.close()` in two Hono `onAbort` listeners. Hono invokes subscribers with a bare `subscriber()`, so a promise returned from the listener is floated one layer up; `closeAbortedStream` owns the rejection where it can actually be owned. - tui (5) — `handleDisconnect` had no catch of its own and its only caller is a key handler that cannot await, so a failing disconnect was a live unhandled rejection; it now surfaces the message the way `handleConnect` does. The rest are `void` at callers that genuinely cannot await, over callees that already own their failures. - web (92) — 83 are Storybook `expect(...)`, which is instrumented and returns a promise, now awaited (`scrollAreaStoryAssertions` is async for the same reason). Two tests were asserting against a provider call they never awaited. `open(url)` in both web servers now warns instead of escaping a synchronous callback. - cli, launcher — already clean. Type-aware linting costs time: `clients/web`'s `eslint .` goes from ~8s to ~19s, inside `validate`. Narrowing the loaded projects is the lever if that needs reducing later; dropping the rule is not. Closes #1959 Signed-off-by: cliffhall <cliff@futurescale.com>
95 lines
4.3 KiB
TypeScript
95 lines
4.3 KiB
TypeScript
import { Button } from "@mantine/core";
|
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
import { expect, within } from "storybook/test";
|
|
import { theme } from "../theme/theme";
|
|
|
|
// Guard for #1898.
|
|
//
|
|
// `.storybook/vitest.setup.ts` used to call `setProjectAnnotations([...])` to
|
|
// hand the preview annotations to the Storybook vitest project. Since Storybook
|
|
// 10.3 `@storybook/addon-vitest` applies them automatically — and *skips* doing
|
|
// so when it finds such a setup file — so the file was removed and the
|
|
// `setupFiles` entry dropped from the `storybook` project in `vite.config.ts`.
|
|
//
|
|
// A green suite is not evidence that the automatic path works. Two things the
|
|
// setup file used to provision are silently losable:
|
|
//
|
|
// • `./preview`'s decorator, which wraps every story in `MantineProvider`
|
|
// with the project theme, and its `import "../src/App.css"`, which defines
|
|
// the `--inspector-*` tokens. Without them stories render unthemed and
|
|
// unstyled — and would very likely still pass their play functions.
|
|
// • `@storybook/addon-a11y/preview`, which runs axe after each play function.
|
|
// Without it the `a11y: { test: "error" }` parameter is inert and every
|
|
// story passes its accessibility check vacuously.
|
|
//
|
|
// The story below closes the first gap: it asserts the Mantine theme variables
|
|
// and the `App.css` tokens are actually present in the rendered document, so an
|
|
// unthemed render fails loudly rather than passing. It also asserts the preview
|
|
// `parameters` reached the story, which is the same channel the a11y parameter
|
|
// arrives on. The axe *runner* itself can't be introspected from a play
|
|
// function; it was verified out-of-band by temporarily giving a story a real
|
|
// violation and confirming the suite went red (see the PR for #1898).
|
|
|
|
// Mantine's own default when a theme pins no `primaryShade` — the value it
|
|
// falls back to when deriving `--mantine-primary-color-filled`.
|
|
const MANTINE_DEFAULT_LIGHT_PRIMARY_SHADE = 6;
|
|
|
|
// The shade Mantine derives `--mantine-primary-color-filled` from. Everything
|
|
// here is read from the theme rather than restated, so re-pinning
|
|
// `primaryShade` or repainting the palette moves the expectation with it — a
|
|
// guard that had to be edited alongside a legitimate theme change would just
|
|
// train people to edit it, which is the opposite of what it's for.
|
|
//
|
|
// The light branch is the one that matters: `./preview` renders every story
|
|
// with `defaultColorScheme="light"` and an `initialGlobals.colorScheme` of
|
|
// `"light"`. `primaryShade` is `number | { light, dark }` in Mantine's types,
|
|
// so both forms are handled.
|
|
function lightPrimaryShade(): number {
|
|
const shade = theme.primaryShade;
|
|
if (typeof shade === "number") return shade;
|
|
return shade?.light ?? MANTINE_DEFAULT_LIGHT_PRIMARY_SHADE;
|
|
}
|
|
|
|
function expectedPrimaryColor(): string {
|
|
const palette = theme.colors?.[theme.primaryColor ?? ""];
|
|
const color = palette?.[lightPrimaryShade()];
|
|
if (!color) throw new Error("theme is missing its primary color palette");
|
|
return color;
|
|
}
|
|
|
|
const meta: Meta<typeof Button> = {
|
|
title: "Meta/Preview Annotations",
|
|
component: Button,
|
|
args: { children: "Themed" },
|
|
};
|
|
|
|
export default meta;
|
|
type Story = StoryObj<typeof Button>;
|
|
|
|
export const ProjectAnnotationsApplied: Story = {
|
|
play: async ({ canvasElement, parameters }) => {
|
|
const canvas = within(canvasElement);
|
|
await expect(
|
|
canvas.getByRole("button", { name: "Themed" }),
|
|
).toBeInTheDocument();
|
|
|
|
const root = getComputedStyle(document.documentElement);
|
|
|
|
// `MantineProvider` (the `./preview` decorator) injects the theme's CSS
|
|
// variables onto `:root`. No provider, no variables.
|
|
await expect(
|
|
root.getPropertyValue("--mantine-primary-color-filled").trim(),
|
|
).toBe(expectedPrimaryColor());
|
|
|
|
// `--inspector-brand-primary` is defined in `App.css`, which only reaches
|
|
// the story through `./preview`'s stylesheet import — and it resolves
|
|
// *through* the Mantine variable above, so this covers both layers.
|
|
await expect(
|
|
root.getPropertyValue("--inspector-brand-primary").trim(),
|
|
).toBe(expectedPrimaryColor());
|
|
|
|
// The preview `parameters` merged into the story context — the same channel
|
|
// that carries `a11y: { test: "error" }` to the a11y addon.
|
|
await expect(parameters.a11y).toMatchObject({ test: "error" });
|
|
},
|
|
};
|