feat: Authenticated Shell, Quick Tabs and Change Password 1.03
This commit is contained in:
@@ -1,221 +0,0 @@
|
|||||||
# Implementation Plan: Authenticated Shell — LED Color Rendering (Job 878, 1.02)
|
|
||||||
|
|
||||||
## 0. Already-Implemented Check
|
|
||||||
|
|
||||||
After investigating the repository:
|
|
||||||
|
|
||||||
- `frontend/src/app/features/shell/header/shell-header.component.ts` already
|
|
||||||
renders `<span class="led" [class.led-running]="..." [class.led-countdown]="..." [class.led-stopped]="..." [class.led-unconfigured]="..." data-testid="shell-led">`
|
|
||||||
with the correct `aria-label`.
|
|
||||||
- The state derivation (`running` / `countdown` / `stopped` / `unconfigured`)
|
|
||||||
is already correct (verified by the Job description: `running` between
|
|
||||||
Start and End is being assigned).
|
|
||||||
- `frontend/src/styles.css` defines the color tokens
|
|
||||||
`--color-success`, `--color-warning`, `--color-danger`, but contains **no**
|
|
||||||
rules for `.led`, `.led-running`, `.led-countdown`, `.led-stopped`, or
|
|
||||||
`.led-unconfigured`.
|
|
||||||
|
|
||||||
The bug is therefore isolated to missing CSS. The fix is purely a stylesheet
|
|
||||||
addition. No TypeScript / Angular code changes are required; no backend, no
|
|
||||||
state logic, and no template changes are required.
|
|
||||||
|
|
||||||
This means most sections of this plan are intentionally short — there is
|
|
||||||
nothing to migrate, no APIs to add, and no component logic to author.
|
|
||||||
|
|
||||||
## 1. Architectural Reconnaissance
|
|
||||||
|
|
||||||
- **Codebase style & conventions:** TypeScript, Angular 17+ standalone
|
|
||||||
components (`ShellHeaderComponent` is `standalone: true`,
|
|
||||||
`ChangeDetectionStrategy.OnPush`). Global styles live in
|
|
||||||
`frontend/src/styles.css` and are the single source of design tokens
|
|
||||||
(`--color-*`, `--radius-*`, `--font-family`). All Angular component
|
|
||||||
templates and styles are inlined per-component; nothing for the LED is
|
|
||||||
defined at component scope.
|
|
||||||
- **Data Layer:** Not applicable — this Job is a pure CSS fix. The event state
|
|
||||||
itself already flows correctly via `EventStatusStore` →
|
|
||||||
`HomeComponent` → `ShellHeaderComponent.eventState` input.
|
|
||||||
- **Test Framework & Structure:** Jest via `tests/jest.config.js` with two
|
|
||||||
projects (`backend`, `frontend`). Frontend tests use `jsdom` + `ts-jest` and
|
|
||||||
`tests/frontend/jest.setup.ts` (provides `matchMedia` shim). Frontend specs
|
|
||||||
live under `tests/frontend/*.spec.ts` and import directly from
|
|
||||||
`frontend/src/...`. All tests run from the repo root with a single command:
|
|
||||||
`npm test`.
|
|
||||||
- **Required Tools & Dependencies:** None new. Existing stack (Node, Angular
|
|
||||||
CLI / `npm --workspace frontend run build`, Jest 29, jsdom 29) is
|
|
||||||
sufficient.
|
|
||||||
|
|
||||||
## 2. Impacted Files
|
|
||||||
|
|
||||||
- **To Modify:**
|
|
||||||
- `frontend/src/styles.css` — add the four `.led-{state}` rules plus the
|
|
||||||
base `.led` rule, using the already-defined color tokens. One-line
|
|
||||||
reason: the LED element currently has no dimensions and no color, so it
|
|
||||||
renders as invisible content; the bug description confirms this
|
|
||||||
exactly.
|
|
||||||
- **To Create:**
|
|
||||||
- `tests/frontend/led-class-mapping.spec.ts` — minimal regression test
|
|
||||||
that asserts `ShellHeaderComponent.ledClass()` produces the expected
|
|
||||||
`led led-<state>` string for each `EventState` value. This verifies the
|
|
||||||
class wiring that the new CSS rules depend on (small, pure, runnable
|
|
||||||
without a UI).
|
|
||||||
|
|
||||||
## 3. Proposed Changes
|
|
||||||
|
|
||||||
### 3.1 Stylesheet (`frontend/src/styles.css`)
|
|
||||||
|
|
||||||
Append the following rules to the bottom of `frontend/src/styles.css`,
|
|
||||||
preserving the existing two-space indentation used in the file:
|
|
||||||
|
|
||||||
```css
|
|
||||||
.led {
|
|
||||||
display: inline-block;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.led-running { background-color: var(--color-success); }
|
|
||||||
.led-countdown { background-color: var(--color-warning); }
|
|
||||||
.led-stopped { background-color: var(--color-danger); }
|
|
||||||
.led-unconfigured { background-color: var(--color-secondary); }
|
|
||||||
```
|
|
||||||
|
|
||||||
Design rationale (each decision is required to satisfy the Job):
|
|
||||||
|
|
||||||
- `display: inline-block` + `width/height: 10px` + `border-radius: 50%` are
|
|
||||||
the minimum declarations needed to paint a visible circular dot and to
|
|
||||||
override the browser's default `display: inline` on the `<span>` (which
|
|
||||||
the bug report explicitly calls out as `width: auto; height: auto;
|
|
||||||
background-color: rgba(0,0,0,0)`).
|
|
||||||
- The four state classes map directly to the four token colors already
|
|
||||||
declared in `:root`, which is exactly the mapping the Job specifies
|
|
||||||
(Green / Yellow / Red / secondary-gray for unconfigured, since
|
|
||||||
`--color-secondary` was the only remaining neutral token available).
|
|
||||||
- No `::before` / `::after` content is used, so no `content` property is
|
|
||||||
needed (the bug report ruled these out as a fix path).
|
|
||||||
- No changes to the template or component logic — the `[class.led-running]`
|
|
||||||
etc. bindings in `shell-header.component.ts` already drive the right
|
|
||||||
state class.
|
|
||||||
|
|
||||||
### 3.2 Regression test (`tests/frontend/led-class-mapping.spec.ts`)
|
|
||||||
|
|
||||||
This test is purely defensive: it locks in the contract that
|
|
||||||
`ShellHeaderComponent.ledClass` produces the class string the CSS rules are
|
|
||||||
keyed on, so a future template refactor cannot silently break the visual
|
|
||||||
fix. It uses the existing `jsdom` Jest project and the same import style as
|
|
||||||
`tests/frontend/shell-active-section.spec.ts`.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { ShellHeaderComponent } from
|
|
||||||
'../../frontend/src/app/features/shell/header/shell-header.component';
|
|
||||||
import { EventState } from
|
|
||||||
'../../frontend/src/app/core/services/event-status.store';
|
|
||||||
|
|
||||||
describe('ShellHeaderComponent LED class mapping', () => {
|
|
||||||
function build() {
|
|
||||||
const c = new ShellHeaderComponent();
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
['running', 'led led-running'],
|
|
||||||
['countdown', 'led led-countdown'],
|
|
||||||
['stopped', 'led led-stopped'],
|
|
||||||
['unconfigured', 'led led-unconfigured'],
|
|
||||||
] as [EventState, string][])(
|
|
||||||
'maps event state %s to class string %s',
|
|
||||||
(state, expected) => {
|
|
||||||
const c = build();
|
|
||||||
c.eventState.set(state as any); // input signal set is a noop — handled below
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Because `ShellHeaderComponent` exposes `eventState` as an Angular **input
|
|
||||||
signal** (`input<EventState>('unconfigured')`), and test-driving Angular
|
|
||||||
signals without `TestBed` is fragile, the cleanest minimal test — and the
|
|
||||||
one most consistent with the existing convention used by
|
|
||||||
`shell-active-section.spec.ts` (which tests a pure helper) — is to mirror
|
|
||||||
that pattern. We will instead add **one lightweight pure helper** export
|
|
||||||
from `shell-header.component.ts`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// pure helper, exported for tests
|
|
||||||
export function ledClassForState(state: EventState): string {
|
|
||||||
return `led led-${state}`;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And have `ledClass` delegate to it:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
readonly ledClass = computed(() => ledClassForState(this.eventState()));
|
|
||||||
```
|
|
||||||
|
|
||||||
This keeps the test pure-function-style (matches the
|
|
||||||
`deriveActiveSectionFromUrl` precedent in `home.shell.ts`) and avoids any
|
|
||||||
`TestBed` / `ChangeDetection` setup. The test then becomes:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { ledClassForState } from
|
|
||||||
'../../frontend/src/app/features/shell/header/shell-header.component';
|
|
||||||
|
|
||||||
describe('ledClassForState', () => {
|
|
||||||
it('returns the base + state class for each EventState', () => {
|
|
||||||
expect(ledClassForState('running'))
|
|
||||||
.toBe('led led-running');
|
|
||||||
expect(ledClassForState('countdown'))
|
|
||||||
.toBe('led led-countdown');
|
|
||||||
expect(ledClassForState('stopped'))
|
|
||||||
.toBe('led led-stopped');
|
|
||||||
expect(ledClassForState('unconfigured'))
|
|
||||||
.toBe('led led-unconfigured');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
A single `describe` / single `it` keeps the suite minimal and instantly
|
|
||||||
runnable, satisfying the "minimal, highly-focused tests" rule.
|
|
||||||
|
|
||||||
If the implementer prefers to avoid touching `shell-header.component.ts`
|
|
||||||
entirely, the alternative is to skip this test. The plan therefore lists
|
|
||||||
this test as **optional / lowest priority**, and the Plan Implementer may
|
|
||||||
omit it without affecting the bug fix. The bug fix does **not** require
|
|
||||||
any code change to `shell-header.component.ts`.
|
|
||||||
|
|
||||||
## 4. Test Strategy
|
|
||||||
|
|
||||||
- **Target Unit Test File:** `tests/frontend/led-class-mapping.spec.ts`
|
|
||||||
(optional; see §3.2). Run via `npm test` from the repo root, which the
|
|
||||||
project's package.json already wires to
|
|
||||||
`jest --config tests/jest.config.js`.
|
|
||||||
- **Mocking Strategy:** None required. The test, if added, exercises a
|
|
||||||
pure exported function with no I/O, no Angular DI, and no DOM mutation,
|
|
||||||
so no mocks, fakes, or `TestBed` configuration are needed. The existing
|
|
||||||
`tests/frontend/jest.setup.ts` already shims `matchMedia` for any
|
|
||||||
incidental DOM usage.
|
|
||||||
- **What we deliberately do NOT add:**
|
|
||||||
- No visual regression test (the Job explicitly forbids tests that
|
|
||||||
require visual confirmation).
|
|
||||||
- No `TestBed` rendering test for `ShellHeaderComponent` (would add
|
|
||||||
infrastructure cost for no functional value, since the existing
|
|
||||||
template + class bindings are unchanged).
|
|
||||||
- No backend or integration test — the bug is purely CSS.
|
|
||||||
|
|
||||||
## 5. Acceptance Criteria
|
|
||||||
|
|
||||||
The Job is considered fixed when, after rebuilding the Angular bundle:
|
|
||||||
|
|
||||||
1. The `<span data-testid="shell-led">` element is rendered as a 10×10
|
|
||||||
filled circle in the authenticated shell header.
|
|
||||||
2. Its background color matches the live event state:
|
|
||||||
- `countdown` (now → startUtc) → `var(--color-warning)` (#f59e0b / yellow)
|
|
||||||
- `running` (startUtc → endUtc) → `var(--color-success)` (#10b981 / green)
|
|
||||||
- `stopped` (after endUtc) → `var(--color-danger)` (#ef4444 / red)
|
|
||||||
- `unconfigured` (no Start/End) → `var(--color-secondary)` neutral
|
|
||||||
3. No new HTTP traffic, no new backend code, no new Angular component
|
|
||||||
file, and no change to the shell template.
|
|
||||||
4. `npm test` continues to pass.
|
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
# Implementation Plan: Authenticated Shell — Component-Scoped LED Fix (Job 879, iteration 1.03)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
The Job 879 reproduction shows that the LED `<span class="led led-running">`
|
||||||
|
renders with zero size and transparent background even though the global
|
||||||
|
`frontend/src/styles.css` declares the rules and the production bundle
|
||||||
|
contains them. To guarantee the rules apply regardless of cascade order,
|
||||||
|
external stylesheet loading, or specificity conflicts, this fix moves the
|
||||||
|
LED's visual styling from the global stylesheet into the
|
||||||
|
`ShellHeaderComponent` itself using Angular's
|
||||||
|
`styles: [...]` (component-scoped CSS).
|
||||||
|
|
||||||
|
The component is a standalone component (no `styleUrls`), so the fix is a
|
||||||
|
single, self-contained edit: add `styles: [\`...\`] to the `@Component`
|
||||||
|
decorator in
|
||||||
|
`frontend/src/app/features/shell/header/shell-header.component.ts`.
|
||||||
|
The existing template, state class bindings, and `aria-label` are kept
|
||||||
|
exactly as-is (lines 26–34).
|
||||||
|
|
||||||
|
A minimal regression test is added in a dedicated
|
||||||
|
`tests/frontend/shell-led.spec.ts` that asserts the rendered `<span>`
|
||||||
|
carries `led` plus the correct `led-<state>` class for each
|
||||||
|
`EventState`, and that the bundled component CSS contains the four state
|
||||||
|
selectors.
|
||||||
|
|
||||||
|
## 1. Architectural Reconnaissance
|
||||||
|
|
||||||
|
- **Codebase style & conventions:**
|
||||||
|
- NestJS 10 backend + Angular 17 standalone components.
|
||||||
|
- Standalone components use `imports: [...]` and prefer `styles: [\`...\`]`
|
||||||
|
for small, co-located CSS. Existing examples in this repo:
|
||||||
|
`frontend/src/app/features/setup/setup-create-admin.component.ts`
|
||||||
|
uses `styleUrls: ['./setup-create-admin.component.css']`;
|
||||||
|
`frontend/src/app/features/landing/landing.component.ts` likewise.
|
||||||
|
For a small block of CSS that belongs to a single component, the
|
||||||
|
inline `styles: [\`...\`] form keeps the fix in one file (per the
|
||||||
|
user instruction: "Update the LED styling in
|
||||||
|
frontend/src/app/features/shell/header/shell-header.component.ts near
|
||||||
|
lines 25-34").
|
||||||
|
- Angular's default `ViewEncapsulation.Emulated` rewrites component CSS
|
||||||
|
selectors with attribute selectors (`[_ngcontent-xxx]`) and adds the
|
||||||
|
same attribute to the component's host + template nodes. Because the
|
||||||
|
LED `<span>` is rendered by `ShellHeaderComponent` (not by a child
|
||||||
|
component or via `innerHTML`), encapsulation correctly scopes the new
|
||||||
|
rules to that span, while still allowing the global `:root` CSS
|
||||||
|
custom-property tokens (`--color-success`, etc.) to be inherited.
|
||||||
|
|
||||||
|
- **Data Layer:** Not impacted. The LED color is driven by
|
||||||
|
`eventState()` from `EventStatusStore` (already wired).
|
||||||
|
|
||||||
|
- **Test Framework & Structure:**
|
||||||
|
- Jest 29 multi-project config (`tests/jest.config.js`).
|
||||||
|
- Frontend project uses `ts-jest` + jsdom +
|
||||||
|
`tests/frontend/jest.setup.ts` (Angular TestBed shims).
|
||||||
|
- Tests MUST live in `tests/frontend/` — never alongside source. They
|
||||||
|
MUST run via `npm test` (single root command). Tests MUST NOT
|
||||||
|
require a UI; this plan keeps them purely structural.
|
||||||
|
|
||||||
|
- **Required Tools & Dependencies:** None new. Existing
|
||||||
|
`@angular-devkit/build-angular:application` builder already supports
|
||||||
|
component-scoped `styles: [\`...\`] `, and `critters` (used for inline
|
||||||
|
above-the-fold CSS) is unaffected. `setup.sh` needs no changes.
|
||||||
|
|
||||||
|
## 2. Impacted Files
|
||||||
|
|
||||||
|
- **To Modify:**
|
||||||
|
- `frontend/src/app/features/shell/header/shell-header.component.ts`
|
||||||
|
— add a `styles: [\`...\`] block to the `@Component` decorator that
|
||||||
|
mirrors the LED rules from the global stylesheet, scoped to the
|
||||||
|
component. Keep the existing template (lines 10–88) untouched.
|
||||||
|
Keep the `[class.led-<state>]` bindings and the dynamic `aria-label`.
|
||||||
|
|
||||||
|
- **To Create:**
|
||||||
|
- `tests/frontend/shell-led.spec.ts` — minimal jsdom-based test that
|
||||||
|
renders `ShellHeaderComponent` with each `EventState` value and
|
||||||
|
asserts (a) the rendered `[data-testid="shell-led"]` carries the
|
||||||
|
`led` class plus the matching `led-<state>` class, and (b) the
|
||||||
|
component's bundled CSS string contains the four required state
|
||||||
|
selectors (`led-running`, `led-countdown`, `led-stopped`,
|
||||||
|
`led-unconfigured`).
|
||||||
|
|
||||||
|
## 3. Proposed Changes
|
||||||
|
|
||||||
|
1. **Component-scoped CSS in `ShellHeaderComponent`**
|
||||||
|
In
|
||||||
|
`frontend/src/app/features/shell/header/shell-header.component.ts`,
|
||||||
|
add a `styles` property to the `@Component` decorator. Place it
|
||||||
|
immediately after the existing `imports: [CommonModule]` property
|
||||||
|
(around the current line 9 area) so the decorator remains in
|
||||||
|
canonical Angular order: `selector`, `standalone`, `changeDetection`,
|
||||||
|
`imports`, `template`, `styles`.
|
||||||
|
|
||||||
|
Exact CSS block to add inside the backtick literal:
|
||||||
|
|
||||||
|
```
|
||||||
|
:host .led,
|
||||||
|
.led {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.led-running { background-color: var(--color-success); }
|
||||||
|
.led-countdown { background-color: var(--color-warning); }
|
||||||
|
.led-stopped { background-color: var(--color-danger); }
|
||||||
|
.led-unconfigured { background-color: var(--color-secondary); }
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The base rule is duplicated as `:host .led, .led` so it matches
|
||||||
|
both a projected host usage and the in-template span. The
|
||||||
|
`:host` selector is harmless under Emulated encapsulation (it
|
||||||
|
targets the component's own host element) and does not break the
|
||||||
|
existing template.
|
||||||
|
- State classes are written exactly as before — the template already
|
||||||
|
toggles `[class.led-running]`, `[class.led-countdown]`,
|
||||||
|
`[class.led-stopped]`, `[class.led-unconfigured]`.
|
||||||
|
- Color values use the existing `:root` CSS custom properties
|
||||||
|
(`--color-success`/`--color-warning`/`--color-danger`/
|
||||||
|
`--color-secondary`) defined in `frontend/src/styles.css`. These
|
||||||
|
tokens are inherited through the DOM, so the component-scoped
|
||||||
|
rules resolve them automatically — no per-state hex is needed in
|
||||||
|
TypeScript.
|
||||||
|
- The current global rules in `frontend/src/styles.css` (lines 23–27)
|
||||||
|
are left untouched as a safety net; they remain correct and
|
||||||
|
harmless.
|
||||||
|
|
||||||
|
2. **No template or state-binding changes**
|
||||||
|
The `<span ... class="led" [class.led-running]="..." ...>`
|
||||||
|
block (lines 26–34) and the dynamic `aria-label` are kept exactly
|
||||||
|
as the documentation in
|
||||||
|
`docs/guides/authenticated-shell.md` specifies.
|
||||||
|
|
||||||
|
3. **No backend / DB changes**
|
||||||
|
The bug is purely a frontend cascade issue.
|
||||||
|
|
||||||
|
## 4. Test Strategy
|
||||||
|
|
||||||
|
- **Target Unit Test File:** `tests/frontend/shell-led.spec.ts`
|
||||||
|
(new file, in the dedicated `tests/frontend/` folder per repo
|
||||||
|
convention; runs via `npm test` -> `jest --config tests/jest.config.js
|
||||||
|
--selectProjects frontend`).
|
||||||
|
|
||||||
|
- **Mocking Strategy:**
|
||||||
|
- Use Angular's `TestBed.configureTestingModule({ imports:
|
||||||
|
[ShellHeaderComponent] })` so the component renders standalone
|
||||||
|
in jsdom with no router / http mocking required (the LED span has
|
||||||
|
no dependencies).
|
||||||
|
- Provide minimal inputs via the component's `input.required<...>` /
|
||||||
|
`input<...>(...)` signals. Specifically, set `eventState()` to
|
||||||
|
each of `'running' | 'countdown' | 'stopped' | 'unconfigured'`
|
||||||
|
using `TestBed.runInInjectionContext` + `ComponentRef.setInput`
|
||||||
|
(or `fixture.componentRef.setInput`) and assert the DOM.
|
||||||
|
- No HTTP, no router, no SSE — keeping the test fast and isolated.
|
||||||
|
- Two compact assertions per case:
|
||||||
|
1. `expect(led.classList.contains('led')).toBe(true)` and the
|
||||||
|
matching `led-<state>` class.
|
||||||
|
2. Read the component's raw `styles` string (via the decorated
|
||||||
|
component's static `ɵcmp.styles` or by string-matching the
|
||||||
|
source file's `styles:` template literal in a separate small
|
||||||
|
test) to confirm all four state selectors are present.
|
||||||
|
Concretely: a single test that imports the component file as
|
||||||
|
text (via `fs.readFileSync` of the TS source — same pattern
|
||||||
|
other frontend tests already use for inspecting contracts) and
|
||||||
|
asserts it contains
|
||||||
|
`.led-running`, `.led-countdown`, `.led-stopped`,
|
||||||
|
`.led-unconfigured`, plus `width: 10px`, `height: 10px`,
|
||||||
|
`border-radius: 50%`, and `var(--color-success)`.
|
||||||
|
|
||||||
|
- **What is NOT tested (per instructions):** no visual / screenshot
|
||||||
|
confirmation, no jsdom layout reflow assertions, no large mock SSE
|
||||||
|
harness — only the structural class binding and the presence of
|
||||||
|
the component-scoped CSS rules. This matches the
|
||||||
|
"minimal, highly-focused tests" instruction.
|
||||||
@@ -7,6 +7,22 @@ import { EventState } from '../../../core/services/event-status.store';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule],
|
||||||
|
styles: [`
|
||||||
|
:host .led,
|
||||||
|
.led {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.led-running { background-color: var(--color-success); }
|
||||||
|
.led-countdown { background-color: var(--color-warning); }
|
||||||
|
.led-stopped { background-color: var(--color-danger); }
|
||||||
|
.led-unconfigured { background-color: var(--color-secondary); }
|
||||||
|
`],
|
||||||
template: `
|
template: `
|
||||||
<header class="shell-header" data-testid="shell-header">
|
<header class="shell-header" data-testid="shell-header">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { EventState } from '../../frontend/src/app/core/services/event-status.store';
|
||||||
|
|
||||||
|
describe('ShellHeaderComponent LED styling (component-scoped)', () => {
|
||||||
|
const sourcePath = resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../frontend/src/app/features/shell/header/shell-header.component.ts',
|
||||||
|
);
|
||||||
|
const source = readFileSync(sourcePath, 'utf8');
|
||||||
|
|
||||||
|
it('declares component-scoped styles with the LED base shape', () => {
|
||||||
|
expect(source).toMatch(/styles:\s*\[/);
|
||||||
|
expect(source).toMatch(/display:\s*inline-block/);
|
||||||
|
expect(source).toMatch(/width:\s*10px/);
|
||||||
|
expect(source).toMatch(/height:\s*10px/);
|
||||||
|
expect(source).toMatch(/border-radius:\s*50%/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['.led-running', '--color-success'],
|
||||||
|
['.led-countdown', '--color-warning'],
|
||||||
|
['.led-stopped', '--color-danger'],
|
||||||
|
['.led-unconfigured', '--color-secondary'],
|
||||||
|
] as const)(
|
||||||
|
'scopes %s to the existing %s token',
|
||||||
|
(selector, token) => {
|
||||||
|
const escaped = selector.replace('.', '\\.');
|
||||||
|
const re = new RegExp(`${escaped}\\s*\\{[^}]*var\\(${token}\\)`);
|
||||||
|
expect(source).toMatch(re);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('keeps the existing state class bindings and aria-label in the template', () => {
|
||||||
|
expect(source).toContain('class="led"');
|
||||||
|
expect(source).toContain('[class.led-running]="eventState() === \'running\'"');
|
||||||
|
expect(source).toContain('[class.led-countdown]="eventState() === \'countdown\'"');
|
||||||
|
expect(source).toContain('[class.led-stopped]="eventState() === \'stopped\'"');
|
||||||
|
expect(source).toContain('[class.led-unconfigured]="eventState() === \'unconfigured\'"');
|
||||||
|
expect(source).toContain("data-testid=\"shell-led\"");
|
||||||
|
expect(source).toContain("[attr.aria-label]=\"'Event status: ' + eventState()\"");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('EventState union covers all four LED classes', () => {
|
||||||
|
it.each(['running', 'countdown', 'stopped', 'unconfigured'] as EventState[])(
|
||||||
|
'recognises %s',
|
||||||
|
(state) => {
|
||||||
|
const expected = `led led-${state}`;
|
||||||
|
expect(expected.startsWith('led ')).toBe(true);
|
||||||
|
expect(expected.endsWith(state)).toBe(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user