Files
iptvnator/tools/nx/check-material-theme-scope.test.mjs
T
4grayandClaude Fable 5.1 ced7386dfa perf(web): render the radio play control as an icon button, not a fab (#1938)
The audio player's play/pause control was the app's only mat-fab. It is now
a mat-icon-button drawn as the same 56px primary circle (icon-button
overrides: 56px state layer, 32px white icon, 50% shape, selection-colour
fill, hover scale), so the eager button chunk no longer carries the fab
directives and the theme no longer emits the fab tokens:
renderer.initialBytes 1,349,048 -> 1,329,249 bytes (-19,799: -13,799 in
button.mjs, -6,028 in the stylesheet), measured locally against the merge
base. The fab also reset the keyboard focus ring with `outline: none`; the
icon button keeps the app ring.

- tools/nx/check-material-theme-scope.mjs: `button` brings button and
  icon-button; `fab` is required only while a runtime template renders
  mat-fab / mat-mini-fab (matFab / matMiniFab), found through the component
  templates the way check-form-field-defaults.mjs resolves them. That
  resolution moves to tools/nx/component-templates.mjs, shared by both.
  Templates know HTML comments only, so the scan strips just those: a `//`
  in text or an attribute value no longer hides a fab on the same line.
- apps/web/src/m3-theme.scss drops `fab` from $material-components.
- apps/web-e2e: the Stalker radio test checks the control's size, shape,
  fill, icon and keyboard focus ring.
- journey-baselines.json: renderer.initialBytes 1,345,807 -> 1,329,249.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-14 19:51:34 +02:00

244 lines
7.6 KiB
JavaScript

import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
compareThemeScope,
requiredThemes,
templateThemeUsers,
themedComponents,
usedEntryPoints,
} from './check-material-theme-scope.mjs';
const lines = (...rows) => rows.join('\n');
test('usedEntryPoints lists each Material entry point with its first importer', () => {
const importers = usedEntryPoints([
{
file: 'a.component.ts',
source: lines(
"import { MatButtonModule } from '@angular/material/button';",
'import {',
' MatDialog,',
"} from '@angular/material/dialog';",
'import { MatIcon } from "@angular/material/icon";'
),
},
{
file: 'b.component.ts',
source: "import { MatButton } from '@angular/material/button';",
},
]);
assert.deepEqual(
[...importers],
[
['button', 'a.component.ts'],
['dialog', 'a.component.ts'],
['icon', 'a.component.ts'],
]
);
});
test('usedEntryPoints counts dynamic imports and ignores type-only ones and comments', () => {
const importers = usedEntryPoints([
{
file: 'lazy-mat-dialog.ts',
source: lines(
"import type { MatDialog } from '@angular/material/dialog';",
"type DialogModule = typeof import('@angular/material/dialog');",
'loadDialogModule = () =>',
" import('@angular/material/dialog');"
),
},
{
file: 'types-only.ts',
source: lines(
"import type { MatTooltip } from '@angular/material/tooltip';",
"// import { MatChipsModule } from '@angular/material/chips';",
"/* import { MatTabsModule } from '@angular/material/tabs'; */"
),
},
]);
assert.deepEqual([...importers], [['dialog', 'lazy-mat-dialog.ts']]);
});
test('requiredThemes always keeps core and expands the button entry point', () => {
const required = requiredThemes(
new Map([
['button', 'a.ts'],
['tooltip', 'b.ts'],
['testing', 'c.ts'],
])
);
assert.deepEqual(
[...required],
[
['core', 'always themed'],
['button', '@angular/material/button in a.ts'],
['icon-button', '@angular/material/button in a.ts'],
['tooltip', '@angular/material/tooltip in b.ts'],
]
);
});
test('requiredThemes adds the themes rendered templates call for', () => {
const required = requiredThemes(
new Map([['button', 'a.ts']]),
new Map([['fab', { file: 'b.component.ts', selector: 'mat-mini-fab' }]])
);
assert.deepEqual(
[...required],
[
['core', 'always themed'],
['button', '@angular/material/button in a.ts'],
['icon-button', '@angular/material/button in a.ts'],
['fab', 'mat-mini-fab in b.component.ts'],
]
);
});
test('templateThemeUsers finds a fab element in the first template that renders one', async () => {
const templates = {
'libs/b/b.component.html':
'<a [disabled]="count > 1" matFab href="#">Up</a>',
};
const users = await templateThemeUsers(
[
{
file: 'libs/a/a.component.ts',
source: lines(
'@Component({',
' template: `',
' <!-- <button mat-fab></button> -->',
' <p title="a > b">Use mat-fab or matMiniFab here</p>',
' <button class="mat-fab mat-mini-fab">mat-fab</button>',
' <button mat-fab-like matFabulous></button>',
' `,',
'})',
'export class A {}'
),
},
{
file: 'libs/b/b.component.ts',
source: lines(
'@Component({',
" templateUrl: './b.component.html',",
'})',
'export class B {}'
),
},
{
file: 'libs/c/c.component.ts',
source: lines(
'@Component({',
' template: `<button mat-mini-fab (click)="go()"></button>`,',
'})',
'export class C {}'
),
},
],
async (file) => templates[file]
);
assert.deepEqual(
[...users],
[['fab', { file: 'libs/b/b.component.ts', selector: 'matFab' }]]
);
});
test('templateThemeUsers strips HTML comments only: a // in text or a URL hides nothing', async () => {
const users = await templateThemeUsers(
[
{
file: 'libs/e/e.component.ts',
source: lines(
'@Component({',
' template: `',
' <!-- <button mat-fab></button> -->',
' <span>now // later</span> <a href="https://x/up" mat-fab>Up</a>',
' <button title="Help // tips" mat-mini-fab></button>',
' `,',
'})',
'export class E {}'
),
},
],
async () => undefined
);
assert.deepEqual(
[...users],
[['fab', { file: 'libs/e/e.component.ts', selector: 'mat-fab' }]]
);
});
test('templateThemeUsers finds nothing without a rendered fab', async () => {
const users = await templateThemeUsers(
[
{
file: 'libs/d/d.component.ts',
source: lines(
'@Component({',
' template: `<button mat-icon-button class="play-btn"></button>`,',
'})',
'export class D {}'
),
},
{ file: 'libs/d/d.service.ts', source: 'export const fab = 1;' },
],
async () => undefined
);
assert.deepEqual([...users], []);
});
test('themedComponents reads the list, direct includes and the all-components mixin', () => {
assert.deepEqual(
themedComponents(
lines(
'$material-components: (',
' core,',
' button, // used by every dialog',
' icon-button,',
');',
'@include mat.tooltip-theme($theme);'
)
),
{
themed: new Set(['core', 'button', 'icon-button', 'tooltip']),
all: false,
}
);
assert.deepEqual(
themedComponents('html { @include mat.all-component-themes($t); }'),
{ themed: new Set(), all: true }
);
});
test('compareThemeScope reports missing and surplus themes', () => {
const required = new Map([
['core', 'always themed'],
['button', '@angular/material/button in a.ts'],
]);
assert.deepEqual(
compareThemeScope(required, {
themed: new Set(['core', 'tabs']),
all: false,
}),
{
unthemed: [['button', '@angular/material/button in a.ts']],
unused: ['tabs'],
}
);
// all-component-themes leaves nothing unthemed, but is still surplus-free
// only when the list is empty.
assert.deepEqual(
compareThemeScope(required, { themed: new Set(), all: true }),
{ unthemed: [], unused: [] }
);
});