From af452fddbc32e85531b2c8e08b74022ed0f73e22 Mon Sep 17 00:00:00 2001 From: pt Date: Sat, 8 Aug 2026 09:14:08 +0200 Subject: [PATCH 1/5] fix(playground): wait for the editor before loading a chapter or an example Run and Reset were gated on the editor having mounted; the controls that LOAD code were not -- they only guarded against a compile being in flight. Loading a chapter is a round-trip to setEditorValue, and before mountEditor has run there is no editor registered for the host, so the call is a silent no-op and mountEditor then installs the starter over the selection. The reader was left with the brief and the chapter highlight showing one chapter while the editor held another, and Run compiled the wrong code and ticked the chapter off as done. On a cold load that window is seconds wide, which is exactly when a first-time reader clicks Tutorial -- it is how I first mis-read the live deploy as broken. Every control now shares one condition, because the bug was two copies of it disagreeing. The regression test asserts the sharing rather than the wording: a new control written with a bare 'Disabled: _busy' fails the fast gate with a message naming the race. The browser E2E cannot catch this as written -- its first action is clicking .pg-run, and Playwright's actionability wait supplies exactly the readiness wait the selection controls were missing. Closes #647 --- CHANGELOG.md | 10 ++++++ .../Rask.Example.Playground/PlaygroundView.cs | 35 +++++++++++++------ .../TutorialPaneStateTests.cs | 28 +++++++++++++++ 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c979d19..b0dbf678 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ them until tagged releases begin. ## [Unreleased] +### Fixed +- **Playground: picking a chapter or an example before the editor had mounted silently kept the starter + code.** Run and Reset waited for the editor; the controls that *load* code did not — they only guarded + against a compile being in flight. Loading a chapter is a round-trip to `setEditorValue`, and before the + editor exists that call is a no-op, so the editor then came up holding the starter instead. The reader + was left with the brief and the chapter highlight showing one chapter while the editor held another — + and Run compiled the wrong code and ticked the chapter off as done. On a cold load the window is seconds + wide, which is exactly when a first-time reader clicks "Tutorial". Every control now shares one gate + (`CanInteract`), since the bug was two copies of the condition disagreeing. Closes #647. + ### Added - **`Mount` — give a component you built yourself the lifecycle it was missing.** A component normally enters the tree through its generated factory, and that factory is what registers the instance with its diff --git a/samples/Rask.Example.Playground/PlaygroundView.cs b/samples/Rask.Example.Playground/PlaygroundView.cs index 227e9b9b..11117a24 100644 --- a/samples/Rask.Example.Playground/PlaygroundView.cs +++ b/samples/Rask.Example.Playground/PlaygroundView.cs @@ -58,9 +58,10 @@ public sealed class PlaygroundView : Component // Progress of the background reference download that powers the IDE features (diagnostics + IntelliSense). private IdeState _ide = IdeState.Loading; - // Set once the editor (Monaco, or the textarea fallback) has mounted. Run stays disabled until then so - // a click can't read an empty editor and compile nothing — and Playwright's actionability wait means - // the E2E naturally waits for the editor without a bespoke sleep. + // Set once the editor (Monaco, or the textarea fallback) has mounted. Every control is disabled until + // then (see CanInteract) so a click can't read an empty editor and compile nothing, or push code into + // an editor that doesn't exist yet — and Playwright's actionability wait means the E2E naturally waits + // for the editor without a bespoke sleep. private bool _editorReady; // Bumps every compile so the preview subtree (and its ErrorBoundary) is keyed fresh — a new run mounts @@ -168,9 +169,9 @@ protected override void OnUnmount() // Reset / Run — the same Bs* button language as the docs; the pg-run class stays a hook // for the Ctrl/Cmd+Enter shortcut (PlaygroundView.js) and the E2E. BsButton(Class: "pg-reset", Color: BsColor.Secondary, Outline: true, Size: BsSize.Sm, - Disabled: _busy || !_editorReady, OnClickAsync: ResetAsync)["Reset"], + Disabled: !CanInteract, OnClickAsync: ResetAsync)["Reset"], BsButton(Class: "pg-run", Color: BsColor.Primary, Size: BsSize.Sm, - Disabled: _busy || !_editorReady || IsActiveChapterLocked, + Disabled: !CanInteract || IsActiveChapterLocked, OnClickAsync: RunAsync)[_busy ? "Running…" : "Run ▸"], // Cross-app links back to the docs + repo, and the shared light/dark toggle. BsLink(Href: "https://pal-tamas.github.io/rask/docs/", Target: "_blank", Rel: "noopener", @@ -222,7 +223,7 @@ private Component TabButton(PlaygroundTab tab, string label) => Class: _tab == tab ? $"{TutorialPaneState.TabClass} {TutorialPaneState.Active}" : TutorialPaneState.TabClass, - Disabled: _busy, + Disabled: !CanInteract, OnClickAsync: () => SwitchTabAsync(tab))[label]; // Switching tabs loads what that tab is pointing at, so the editor always holds the thing the pane @@ -246,7 +247,7 @@ private Component SampleList() => Button( Key: s.Id, Class: s.Id == _activeSampleId ? "pg-example is-active" : "pg-example", - Disabled: _busy, + Disabled: !CanInteract, OnClickAsync: () => SelectSampleAsync(s))[ Span(Class: "pg-example-title")[s.Title], Span(Class: "pg-example-blurb")[s.Blurb] @@ -262,7 +263,7 @@ private Component ChapterList() => Class: TutorialPaneState.ClassesFor(StateOf(c), _completedChapters.Contains(c.Id)), // A locked chapter still opens — the code is worth reading even where it can't run. // Run is what gets disabled for it (see IsActiveChapterLocked). - Disabled: _busy, + Disabled: !CanInteract, OnClickAsync: () => SelectChapterAsync(c))[ Span(Class: "pg-chapter-no")[c.Number.ToString(CultureInfo.InvariantCulture)], Span(Class: "pg-chapter-title")[c.Title], @@ -282,6 +283,20 @@ private ChapterState StateOf(TutorialChapter chapter) : ChapterState.Open; } + // The one gate every control shares: nothing may be clicked while a compile is in flight, and nothing + // may be clicked before the editor exists. + // + // The second half is the subtle one and used to be missing from the selection controls (#647). Loading + // a chapter or an example is a JS round-trip to setEditorValue, and before mountEditor has run there is + // no editor registered for the host, so that call is a silent no-op — mountEditor then finishes and + // installs the starter over the selection. The reader was left with the brief and the highlight showing + // one chapter while the editor held another, and Run cheerfully compiled the wrong code and ticked the + // chapter off. On a cold load the window is seconds wide, which is exactly when a first-time reader + // clicks "Tutorial". + // + // One property rather than six copies of the condition: the bug was two of those copies disagreeing. + private bool CanInteract => !_busy && _editorReady; + // True when the editor holds a chapter this build can't compile. Run is disabled rather than left to // fill the preview with CS0246s about DbContext — the reader can still read the code. private bool IsActiveChapterLocked => @@ -307,10 +322,10 @@ private Component Brief() ], Div(Class: "pg-brief-nav")[ BsButton(Class: "pg-prev", Color: BsColor.Secondary, Outline: true, Size: BsSize.Sm, - Disabled: _busy || chapter.Number == 1, + Disabled: !CanInteract || chapter.Number == 1, OnClickAsync: () => StepAsync(-1))["← Back"], BsButton(Class: "pg-next", Color: BsColor.Secondary, Outline: true, Size: BsSize.Sm, - Disabled: _busy || chapter.Number == TutorialChapters.All.Count, + Disabled: !CanInteract || chapter.Number == TutorialChapters.All.Count, OnClickAsync: () => StepAsync(1))["Next →"] ] ], diff --git a/tests/Rask.Example.Playground.Tests/TutorialPaneStateTests.cs b/tests/Rask.Example.Playground.Tests/TutorialPaneStateTests.cs index 8f97c877..7e399c87 100644 --- a/tests/Rask.Example.Playground.Tests/TutorialPaneStateTests.cs +++ b/tests/Rask.Example.Playground.Tests/TutorialPaneStateTests.cs @@ -126,6 +126,34 @@ public void The_view_locks_the_data_chapters_when_the_build_ships_without_them() Assert.Contains("ChapterState.Locked", view, StringComparison.Ordinal); } + // #647: Run and Reset were gated on the editor having mounted, but the controls that LOAD code were + // not — they only guarded against a compile being in flight. Loading a chapter is a JS round-trip to + // setEditorValue, and before mountEditor has run there is no editor for the host, so the call is a + // silent no-op and mountEditor then installs the starter over the selection: the brief says one + // chapter, the editor holds another, and Run compiles the wrong code and ticks the chapter off. + // + // The fix is one shared condition. This asserts the condition is genuinely shared — a new control + // written with the old bare `Disabled: _busy` is the exact regression, and it re-opens the race. + [Fact] + public void Every_control_is_gated_on_the_editor_being_ready_not_just_on_busy() + { + var view = ReadView(); + + Assert.Contains("private bool CanInteract => !_busy && _editorReady;", view, StringComparison.Ordinal); + + var bare = Regex.Matches(view, @"Disabled: _busy\b").Count; + Assert.True( + bare == 0, + $"{bare} control(s) still gate only on _busy. Loading code into an editor that has not mounted " + + "is a silent no-op (#647) — use `Disabled: !CanInteract` so the control waits for the editor, " + + "as Run and Reset already do."); + + // And every control really is gated: one per Disabled: site, all of them through CanInteract. + var disabled = Regex.Matches(view, @"Disabled: ").Count; + var gated = Regex.Matches(view, @"Disabled: !CanInteract").Count; + Assert.Equal(disabled, gated); + } + private static string ReadView() => File.ReadAllText(Path.Combine(_repoRoot, "samples", "Rask.Example.Playground", "PlaygroundView.cs")); From 7b93f449b59501caf95ff1f93531c34966c63868 Mon Sep 17 00:00:00 2001 From: pt Date: Sat, 8 Aug 2026 09:53:27 +0200 Subject: [PATCH 2/5] fix(e2e): clear the playground's TFM intermediates before its native publish The E2E gate builds the whole solution with -p:WasmBuildNative=false and then publishes the playground WITH the native relink, which #643 introduced and whose comment claimed was safe because the fixture serves the publish output. It is not: that makes the playground the one project built two ways into one obj/, exactly what the note further up the file warns about. The casualty is the scoped-asset bake. Staged under obj/Release/net10.0-browser/rask-scoped/_rask/a, it is left over from the no-native build and never reaches the publish: wwwroot/_rask/ is absent, the page 404s on the PlaygroundView.js that owns mountEditor, the editor never mounts, and every journey then dies waiting on a permanently disabled Run button. It reads as a hang, names no cause, and reproduces only depending on which mode wrote obj/ last -- so it looks like flakiness. A single clean run passes, which is what hid it; the gate has to be run twice in a row to see it. Clearing only obj/Release/net10.0-browser keeps obj/project.assets.json, so the re-restore the publish already does stays incremental. Verified by running the full gate twice back to back: 57 passed, then 57 passed. --- scripts/run-e2e-local.sh | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/scripts/run-e2e-local.sh b/scripts/run-e2e-local.sh index 9d3685dd..a454041c 100755 --- a/scripts/run-e2e-local.sh +++ b/scripts/run-e2e-local.sh @@ -51,10 +51,17 @@ dotnet publish samples/Rask.Example.Shop -c Release --no-build --no-restore --no # The playground is the one sample published WITH the native relink, and it has to be: its tutorial track # runs EF Core against SQLite in the browser, which means linking e_sqlite3 (a static archive) into the # runtime. Passing -p:WasmBuildNative=false here would silently drop the data packages (see -# RaskPlaygroundData in its csproj) and the tutorial half of PlaygroundExampleTests would fail. This does -# not reintroduce the fingerprint/SRI drift the note above warns about: that is one project built two ways -# into one obj/, whereas the fixture serves this publish output, which is internally consistent. +# RaskPlaygroundData in its csproj) and the tutorial half of PlaygroundExampleTests would fail. # +# Which makes this project the exact case the note above warns about — one project built two ways into one +# obj/ — so it gets the TFM intermediates cleared first. Without this the scoped-asset bake, staged under +# obj/Release/net10.0-browser/rask-scoped/_rask/a, is left over from the no-native build and does NOT make +# it into the publish: wwwroot/_rask/ is simply absent, the page 404s on the PlaygroundView.js that owns +# mountEditor, the editor never mounts, and every journey dies waiting for a permanently disabled Run +# button. It reads as a hang, names nothing, and reproduces only on some runs — whichever mode wrote obj/ +# last. Clearing only obj/Release/net10.0-browser keeps obj/project.assets.json, so the restore below is +# still incremental. +rm -rf samples/Rask.Example.Playground/obj/Release/net10.0-browser # It also RE-RESTORES (no --no-restore, unlike the publishes above). RaskPlaygroundData gates the EF Core / # SQLitePCLRaw PackageReferences, so the package graph differs between the two modes — and the build above # restored in the other one. MSBuild does not error when a PackageReference appears after restore, it From 118ef646e6eb5591addc8d93a573362109e83a48 Mon Sep 17 00:00:00 2001 From: pt Date: Sat, 8 Aug 2026 10:02:15 +0200 Subject: [PATCH 3/5] fix(playground): report a missing editor module instead of hanging on a disabled button Mounting the editor is an interop call into the scoped PlaygroundView.js. If that module never loaded, the call never SETTLES -- which is not the same as failing, so mountEditor's textarea fallback never gets a chance and _editorReady is never set. Every control then sits disabled forever with nothing said. That is not hypothetical: a build that bakes no scoped assets produces exactly this bundle, and it reads as 'the playground is broken', sending you to debug Roslyn or Monaco rather than the build (#650, and the previous commit here). The mount now has a deadline -- 60s, generous enough that a slow connection fetching Monaco cannot trip it -- and on expiry says the module did not load and flips the IDE badge to unavailable. Controls stay disabled, because without the module the editor's contents cannot be read either; the change is that the page now names the cause. Verified against a bundle with wwwroot/_rask deleted: the phase line reads 'The editor module (PlaygroundView.js) did not load' within the deadline. --- CHANGELOG.md | 7 +++++ .../Rask.Example.Playground/PlaygroundView.cs | 26 ++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0dbf678..57062cd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,13 @@ them until tagged releases begin. and Run compiled the wrong code and ticked the chapter off as done. On a cold load the window is seconds wide, which is exactly when a first-time reader clicks "Tutorial". Every control now shares one gate (`CanInteract`), since the bug was two copies of the condition disagreeing. Closes #647. +- **Playground: a bundle whose scoped assets are missing now says so, instead of looking hung.** Mounting + the editor is an interop call into `PlaygroundView.js`; if that module never loaded, the call never + *settles* — which is not the same as failing, and the textarea fallback never gets a chance. Every + control then sat disabled forever with no explanation, which reads as "the playground is broken" and + sends you to debug Roslyn or Monaco rather than the build that dropped the assets. The mount now has a + deadline (generous, so a slow connection fetching Monaco can't trip it) and reports the module as + missing. See #650 for the build-side glitch that produces such a bundle. ### Added - **`Mount` — give a component you built yourself the lifecycle it was missing.** A component normally diff --git a/samples/Rask.Example.Playground/PlaygroundView.cs b/samples/Rask.Example.Playground/PlaygroundView.cs index 11117a24..0c21c0d7 100644 --- a/samples/Rask.Example.Playground/PlaygroundView.cs +++ b/samples/Rask.Example.Playground/PlaygroundView.cs @@ -110,9 +110,29 @@ protected override async Task OnRenderedAsync(bool firstRender) // The editor host div now exists in the DOM; create the Monaco editor inside it. mountEditor never // throws (it falls back to a textarea), so reaching here means the editor is usable. - await _js.InvokeVoidAsync("Rask.PlaygroundView.mountEditor", _editorHost, PlaygroundSamples.Starter); - _editorReady = true; - _phase = "Press Run to compile."; + // + // The timeout covers the case that promise never SETTLES, which is different from failing: if the + // scoped PlaygroundView.js module didn't load at all, there is nothing to resolve the invoke, so + // without a deadline _editorReady stays false forever and every control sits disabled with no + // explanation. That has happened for real — a build that silently baked no scoped assets — and it + // reads as "the playground is broken", sending you to debug Roslyn or Monaco rather than the build + // (#650). Generous, because a slow connection fetching Monaco must not trip it. + try + { + await _js.InvokeVoidAsync( + "Rask.PlaygroundView.mountEditor", TimeSpan.FromSeconds(60), _editorHost, + PlaygroundSamples.Starter); + _editorReady = true; + _phase = "Press Run to compile."; + } + catch (Exception ex) when (ex is TaskCanceledException or JSException or JSDisconnectedException) + { + // Everything stays disabled — without the module we cannot even read the editor's contents — + // but now it says why instead of looking like a hung page. + _phase = "The editor module (PlaygroundView.js) did not load — try reloading the page."; + _ide = IdeState.Unavailable; + return; + } // Kick off the (multi-MB) reference download in the background so IntelliSense + live diagnostics come // alive a few seconds after load — without blocking first paint or the first Run. Fire-and-forget: From d261df5aaded3aafe1e8561656db155a52be9e99 Mon Sep 17 00:00:00 2001 From: pt Date: Sat, 8 Aug 2026 10:05:30 +0200 Subject: [PATCH 4/5] fix(build): run the pre-commit gate for samples/ too The hook's change filter listed src/, tests/, benchmarks/ and the build files, so a sample-only commit printed 'no code changes staged' and skipped both dotnet format and the unit suite -- despite samples being compiled, analyzer-checked, warnings-as-errors code with their own test projects. Noticed when the previous commit here, a real change to the playground's C#, skipped the gate. The playground tutorial feature was largely a samples/ change, so most of it went through ungated; it only stayed clean because the gate was run by hand each time, which a contributor has no reason to do. --- .githooks/pre-commit | 6 +++++- CHANGELOG.md | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 2550b6d4..968d9ebd 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -15,7 +15,11 @@ if [ "${RASK_SKIP_UNIT:-}" = "1" ]; then exit 0 fi -if ! git diff --cached --name-only | grep -qE '^(src/|tests/|benchmarks/|Rask\.slnx$|Directory\.)'; then +# samples/ is in here for the same reason src/ is: it is compiled, analyzer-checked, warnings-as-errors +# code with its own test projects (Rask.Example.Playground.Tests, Rask.Example.Shared.Tests, …). Leaving it +# out meant a sample-only commit — a whole feature, in the case of the playground tutorial — skipped the +# format + unit gate entirely and reported "no code changes staged". +if ! git diff --cached --name-only | grep -qE '^(src/|samples/|tests/|benchmarks/|Rask\.slnx$|Directory\.)'; then echo "pre-commit: no code changes staged — skipping the format + unit gate." exit 0 fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 57062cd2..e6d9810f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,11 @@ them until tagged releases begin. sends you to debug Roslyn or Monaco rather than the build that dropped the assets. The mount now has a deadline (generous, so a slow connection fetching Monaco can't trip it) and reports the module as missing. See #650 for the build-side glitch that produces such a bundle. +- **The pre-commit gate now covers `samples/`.** Its change filter listed `src/`, `tests/`, `benchmarks/` + and the build files, so a sample-only commit reported "no code changes staged" and skipped formatting + and the unit suite — even though samples are compiled, analyzer-checked, warnings-as-errors code with + their own test projects. An entire feature landing under `samples/` (the playground tutorial, say) went + through ungated. ### Added - **`Mount` — give a component you built yourself the lifecycle it was missing.** A component normally From c454dc55429b24c280b6df5a5b373b826e927abb Mon Sep 17 00:00:00 2001 From: pt Date: Sat, 8 Aug 2026 10:13:40 +0200 Subject: [PATCH 5/5] fix(build): gate docs/ too, and say why samples/ and docs/ are gated Extends the previous commit. docs/ was missing from the filter for the same reason samples/ was, and with the same consequence: DocsIndexTests walks docs/**/*.md on disk for reachability from docs/README.md, and GuidesTests holds the GuideCatalog parity guard in both directions, so a page committed on its own skipped exactly the checks written to catch it. The samples/ case is likewise worse than a missed format run: Rask.Example.Shared.Tests compiles samples/Rask.Example.Shared and owns DemoMarkup.golden.txt, so a samples-only commit could break a committed golden with nothing objecting until someone else pushed. The comment now states the reason for each rather than listing directories, since the next person to add one needs the principle, not the list. --- .githooks/pre-commit | 18 +++++++++++++----- CHANGELOG.md | 13 ++++++++----- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 968d9ebd..5d493df4 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -15,11 +15,19 @@ if [ "${RASK_SKIP_UNIT:-}" = "1" ]; then exit 0 fi -# samples/ is in here for the same reason src/ is: it is compiled, analyzer-checked, warnings-as-errors -# code with its own test projects (Rask.Example.Playground.Tests, Rask.Example.Shared.Tests, …). Leaving it -# out meant a sample-only commit — a whole feature, in the case of the playground tutorial — skipped the -# format + unit gate entirely and reported "no code changes staged". -if ! git diff --cached --name-only | grep -qE '^(src/|samples/|tests/|benchmarks/|Rask\.slnx$|Directory\.)'; then +# samples/ and docs/ are in here for the same reason src/ is: the suite genuinely gates them. +# +# samples/ is compiled, analyzer-checked, warnings-as-errors code with its own test projects. More than +# that, Rask.Example.Shared.Tests compiles samples/Rask.Example.Shared and owns DemoMarkup.golden.txt — +# so a samples-only commit could break a committed golden with nothing objecting. +# +# docs/ is an input to the tests, not just prose: DocsIndexTests walks docs/**/*.md on disk for +# reachability from docs/README.md, and GuidesTests holds the GuideCatalog parity guard in both +# directions. A new page committed on its own skipped exactly the checks written to catch it. +# +# Leaving these out meant a whole feature could land ungated — the playground tutorial was largely a +# samples/ + docs/ change, and reported "no code changes staged". +if ! git diff --cached --name-only | grep -qE '^(src/|samples/|docs/|tests/|benchmarks/|Rask\.slnx$|Directory\.)'; then echo "pre-commit: no code changes staged — skipping the format + unit gate." exit 0 fi diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d9810f..f3293eea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,11 +23,14 @@ them until tagged releases begin. sends you to debug Roslyn or Monaco rather than the build that dropped the assets. The mount now has a deadline (generous, so a slow connection fetching Monaco can't trip it) and reports the module as missing. See #650 for the build-side glitch that produces such a bundle. -- **The pre-commit gate now covers `samples/`.** Its change filter listed `src/`, `tests/`, `benchmarks/` - and the build files, so a sample-only commit reported "no code changes staged" and skipped formatting - and the unit suite — even though samples are compiled, analyzer-checked, warnings-as-errors code with - their own test projects. An entire feature landing under `samples/` (the playground tutorial, say) went - through ungated. +- **The pre-commit gate now covers `samples/` and `docs/`.** Its change filter listed `src/`, `tests/`, + `benchmarks/` and the build files, so a commit touching only samples or only docs reported "no code + changes staged" and skipped both formatting and the unit suite. That is not merely a missed format run: + `Rask.Example.Shared.Tests` compiles `samples/Rask.Example.Shared` and owns a committed markup golden, + and `DocsIndexTests` / `GuidesTests` read `docs/**/*.md` off disk for reachability and catalog parity — + so a samples-only or docs-only commit could break a golden or a docs invariant with nothing objecting + until somebody else's push. An entire feature could land ungated; the playground tutorial was largely a + `samples/` + `docs/` change. ### Added - **`Mount` — give a component you built yourself the lifecycle it was missing.** A component normally