import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { test } from 'node:test'; import { findNamelessIconButtons, listScannedFiles, } from './check-icon-button-names.mjs'; const nameless = (file, source) => findNamelessIconButtons(file, source).map(({ line, icon }) => ({ line, icon, })); test('reports an icon-only button on the line its tag opens', () => { const template = [ '
', ' ', '
', ].join('\n'); assert.deepEqual(nameless('a.html', template), [ { line: 2, icon: 'close' }, ]); }); test('does not accept a tooltip or a title as the name', () => { const template = [ '', '', '', ].join('\n'); assert.deepEqual(nameless('b.html', template), [ { line: 1, icon: 'close' }, { line: 4, icon: 'close' }, { line: 7, icon: 'close' }, ]); }); test('accepts aria-label, its bindings and aria-labelledby', () => { const template = [ '', '', '', '', '', '', ].join('\n'); assert.deepEqual(nameless('c.html', template), []); }); test('rejects an empty static aria-label', () => { const template = ''; assert.deepEqual(nameless('d.html', template), [ { line: 1, icon: 'close' }, ]); }); test('accepts a button whose content includes text', () => { const template = [ '', "", '', '', ].join('\n'); assert.deepEqual(nameless('e.html', template), []); }); test('ignores text and components hidden from assistive technology', () => { const template = [ '', '', '', '', ].join('\n'); assert.deepEqual(nameless('e2.html', template), [ { line: 1, icon: 'close' }, { line: 2, icon: 'star' }, ]); }); test('looks through control flow, wrappers and spinners', () => { const template = [ '', '', '', '', ].join('\n'); assert.deepEqual(nameless('f.html', template), [ { line: 1, icon: 'sync' }, { line: 8, icon: 'play_arrow / pause' }, { line: 14, icon: 'star' }, { line: 15, icon: '{{ icon() }}' }, ]); }); test('leaves content it cannot see into to the content', () => { const template = [ '', '', '', '', '', 'home', ].join('\n'); assert.deepEqual(nameless('g.html', template), []); }); test('finds buttons nested in other elements and blocks', () => { const template = [ '@for (row of rows; track row.id) {', '
', ' ', '
', '}', '@defer { }', ].join('\n'); assert.deepEqual(nameless('h.html', template), [ { line: 3, icon: 'delete' }, { line: 6, icon: 'info' }, ]); }); test('checks inline component templates on their source lines', () => { const source = [ "import { Component } from '@angular/core';", '@Component({', " selector: 'app-a',", ' template: `', '

Release

', ' ', ' `,', '})', 'export class A {}', "@Component({ selector: 'b', template: '' })", 'export class B {}', '@Component({ selector: "c", template: `` })', "const notAComponent = { template: '' };", ].join('\n'); assert.deepEqual(nameless('a.component.ts', source), [ { line: 6, icon: 'chevron_left' }, { line: 12, icon: 'x' }, ]); }); test('selects tracked Angular app and lib sources, but not specs', async () => { const rootDir = await mkdtemp(path.join(os.tmpdir(), 'button-guard-')); const git = (...args) => execFileSync('git', args, { cwd: rootDir, stdio: 'pipe' }); try { git('init', '-q'); const files = { 'libs/ui/feature/src/lib/deep/panel.component.html': '', 'libs/ui/feature/src/lib/deep/panel.component.ts': '', 'libs/ui/feature/src/lib/deep/panel.component.spec.ts': '', 'apps/web/src/app/app.component.html': '', 'apps/remote-control-web/src/app/app.ts': '', 'apps/website/src/page.html': '', 'apps/electron-backend/src/main.ts': '', 'tools/outside.ts': '', }; for (const [file, content] of Object.entries(files)) { await mkdir(path.dirname(path.join(rootDir, file)), { recursive: true, }); await writeFile(path.join(rootDir, file), content); } git('add', '.'); assert.deepEqual(listScannedFiles(rootDir).sort(), [ 'apps/remote-control-web/src/app/app.ts', 'apps/web/src/app/app.component.html', 'libs/ui/feature/src/lib/deep/panel.component.html', 'libs/ui/feature/src/lib/deep/panel.component.ts', ]); } finally { await rm(rootDir, { recursive: true, force: true }); } });