From 3afd517a6224938f50bbaf1a94e59bdb3cbbe8df Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 13:56:10 +0000 Subject: [PATCH 1/5] feat(components/theme,i18n): add test harnesses for theme and i18n libraries - Add @skyux/theme/testing package with SkyThemeHarness for testing theme directives - Add @skyux/i18n/testing package with SkyResourcesHarness for testing localized content - Update tsconfig.base.json with testing package path mappings - Include comprehensive unit tests for both harnesses Co-Authored-By: benc@cognition.ai --- libs/components/i18n/testing/eslint.config.js | 5 + libs/components/i18n/testing/karma.conf.js | 19 ++ libs/components/i18n/testing/ng-package.json | 5 + libs/components/i18n/testing/project.json | 47 ++++ .../modules/i18n/resources-harness-filters.ts | 7 + .../modules/i18n/resources-harness.spec.ts | 135 +++++++++++ .../src/modules/i18n/resources-harness.ts | 50 ++++ .../components/i18n/testing/src/public-api.ts | 2 + .../i18n/testing/tsconfig.spec.json | 8 + .../components/theme/testing/eslint.config.js | 5 + libs/components/theme/testing/karma.conf.js | 19 ++ libs/components/theme/testing/ng-package.json | 5 + libs/components/theme/testing/project.json | 47 ++++ .../modules/theme/theme-harness-filters.ts | 7 + .../src/modules/theme/theme-harness.spec.ts | 226 ++++++++++++++++++ .../src/modules/theme/theme-harness.ts | 94 ++++++++ .../theme/testing/src/public-api.ts | 2 + .../theme/testing/tsconfig.spec.json | 8 + tsconfig.base.json | 6 + 19 files changed, 697 insertions(+) create mode 100644 libs/components/i18n/testing/eslint.config.js create mode 100644 libs/components/i18n/testing/karma.conf.js create mode 100644 libs/components/i18n/testing/ng-package.json create mode 100644 libs/components/i18n/testing/project.json create mode 100644 libs/components/i18n/testing/src/modules/i18n/resources-harness-filters.ts create mode 100644 libs/components/i18n/testing/src/modules/i18n/resources-harness.spec.ts create mode 100644 libs/components/i18n/testing/src/modules/i18n/resources-harness.ts create mode 100644 libs/components/i18n/testing/src/public-api.ts create mode 100644 libs/components/i18n/testing/tsconfig.spec.json create mode 100644 libs/components/theme/testing/eslint.config.js create mode 100644 libs/components/theme/testing/karma.conf.js create mode 100644 libs/components/theme/testing/ng-package.json create mode 100644 libs/components/theme/testing/project.json create mode 100644 libs/components/theme/testing/src/modules/theme/theme-harness-filters.ts create mode 100644 libs/components/theme/testing/src/modules/theme/theme-harness.spec.ts create mode 100644 libs/components/theme/testing/src/modules/theme/theme-harness.ts create mode 100644 libs/components/theme/testing/src/public-api.ts create mode 100644 libs/components/theme/testing/tsconfig.spec.json diff --git a/libs/components/i18n/testing/eslint.config.js b/libs/components/i18n/testing/eslint.config.js new file mode 100644 index 0000000000..b4c331fb6a --- /dev/null +++ b/libs/components/i18n/testing/eslint.config.js @@ -0,0 +1,5 @@ +const prettier = require('eslint-config-prettier'); +const baseConfig = require('../../../../eslint-base.config'); +const overrides = require('../../../../eslint-overrides.config'); + +module.exports = [...baseConfig, ...overrides, prettier]; diff --git a/libs/components/i18n/testing/karma.conf.js b/libs/components/i18n/testing/karma.conf.js new file mode 100644 index 0000000000..20953f5107 --- /dev/null +++ b/libs/components/i18n/testing/karma.conf.js @@ -0,0 +1,19 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +const { join } = require('path'); +const getBaseKarmaConfig = require('../../../../karma.conf'); + +module.exports = function (config) { + const baseConfig = getBaseKarmaConfig(); + config.set({ + ...baseConfig, + coverageReporter: { + ...baseConfig.coverageReporter, + dir: join( + __dirname, + '../../../../coverage/libs/components/i18n/testing', + ), + }, + }); +}; diff --git a/libs/components/i18n/testing/ng-package.json b/libs/components/i18n/testing/ng-package.json new file mode 100644 index 0000000000..fbafcc4448 --- /dev/null +++ b/libs/components/i18n/testing/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/libs/components/i18n/testing/project.json b/libs/components/i18n/testing/project.json new file mode 100644 index 0000000000..ef9f8aa86f --- /dev/null +++ b/libs/components/i18n/testing/project.json @@ -0,0 +1,47 @@ +{ + "name": "i18n-testing", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "libs/components/i18n/testing/src", + "prefix": "sky", + "tags": ["testing"], + "targets": { + "test": { + "executor": "@angular-devkit/build-angular:karma", + "options": { + "tsConfig": "libs/components/i18n/testing/tsconfig.spec.json", + "karmaConfig": "libs/components/i18n/testing/karma.conf.js", + "codeCoverage": true, + "codeCoverageExclude": ["**/fixtures/**"], + "styles": [ + "libs/components/theme/src/lib/styles/sky.scss", + "libs/components/theme/src/lib/styles/themes/modern/styles.scss" + ], + "polyfills": [ + "zone.js", + "zone.js/testing", + "libs/components/packages/src/polyfills.js" + ], + "inlineStyleLanguage": "scss", + "stylePreprocessorOptions": { + "includePaths": ["{workspaceRoot}"] + } + }, + "configurations": { + "ci": { + "browsers": "ChromeHeadlessNoSandbox", + "codeCoverage": true, + "progress": false, + "sourceMap": true, + "watch": false + } + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "options": { + "lintFilePatterns": ["{projectRoot}/src/**/*.ts"] + } + } + } +} diff --git a/libs/components/i18n/testing/src/modules/i18n/resources-harness-filters.ts b/libs/components/i18n/testing/src/modules/i18n/resources-harness-filters.ts new file mode 100644 index 0000000000..d6a897e060 --- /dev/null +++ b/libs/components/i18n/testing/src/modules/i18n/resources-harness-filters.ts @@ -0,0 +1,7 @@ +import { SkyHarnessFilters } from '@skyux/core/testing'; + +/** + * A set of criteria that can be used to filter a list of `SkyResourcesHarness` instances. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type +export interface SkyResourcesHarnessFilters extends SkyHarnessFilters {} diff --git a/libs/components/i18n/testing/src/modules/i18n/resources-harness.spec.ts b/libs/components/i18n/testing/src/modules/i18n/resources-harness.spec.ts new file mode 100644 index 0000000000..a06750647d --- /dev/null +++ b/libs/components/i18n/testing/src/modules/i18n/resources-harness.spec.ts @@ -0,0 +1,135 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyI18nModule, SkyLibResourcesService } from '@skyux/i18n'; + +import { SkyResourcesHarness } from './resources-harness'; + +@Component({ + selector: 'sky-resources-test', + template: ` + {{ resourceText }} + {{ + resourceText + }} + `, + standalone: false, +}) +class TestComponent { + public resourceText = 'Hello World'; + public titleText = 'Test Title'; +} + +describe('Resources harness', () => { + async function setupTest(options: { dataSkyId?: string } = {}): Promise<{ + harness: SkyResourcesHarness; + fixture: ComponentFixture; + loader: HarnessLoader; + }> { + await TestBed.configureTestingModule({ + declarations: [TestComponent], + imports: [SkyI18nModule], + providers: [SkyLibResourcesService], + }).compileComponents(); + + const fixture = TestBed.createComponent(TestComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + let harness: SkyResourcesHarness; + + if (options.dataSkyId) { + harness = await loader.getHarness( + SkyResourcesHarness.with({ + dataSkyId: options.dataSkyId, + }), + ); + } else { + harness = await loader.getHarness(SkyResourcesHarness); + } + + return { harness, fixture, loader }; + } + + it('should get the text content of the element', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'test-resources', + }); + + fixture.detectChanges(); + + await expectAsync(harness.getText()).toBeResolvedTo('Hello World'); + }); + + it('should get updated text content when resource text changes', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'test-resources', + }); + + fixture.detectChanges(); + + await expectAsync(harness.getText()).toBeResolvedTo('Hello World'); + + fixture.componentInstance.resourceText = 'Updated Text'; + fixture.detectChanges(); + + await expectAsync(harness.getText()).toBeResolvedTo('Updated Text'); + }); + + it('should get the inner HTML of the element', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'test-resources', + }); + + fixture.detectChanges(); + + const innerHTML = await harness.getInnerHtml(); + expect(innerHTML).toContain('Hello World'); + }); + + it('should get an attribute value from the element', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'test-resources-with-attr', + }); + + fixture.detectChanges(); + + await expectAsync(harness.getAttribute('title')).toBeResolvedTo( + 'Test Title', + ); + }); + + it('should return null for non-existent attribute', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'test-resources', + }); + + fixture.detectChanges(); + + await expectAsync( + harness.getAttribute('non-existent-attr'), + ).toBeResolvedTo(null); + }); + + it('should find harness without dataSkyId filter', async () => { + const { harness, fixture } = await setupTest(); + + fixture.detectChanges(); + + await expectAsync(harness.getText()).toBeResolvedTo('Hello World'); + }); + + it('should get all harnesses matching the selector', async () => { + const { loader, fixture } = await setupTest(); + + fixture.detectChanges(); + + const harnesses = await loader.getAllHarnesses(SkyResourcesHarness); + + expect(harnesses.length).toBe(2); + }); +}); diff --git a/libs/components/i18n/testing/src/modules/i18n/resources-harness.ts b/libs/components/i18n/testing/src/modules/i18n/resources-harness.ts new file mode 100644 index 0000000000..bc0228ddb8 --- /dev/null +++ b/libs/components/i18n/testing/src/modules/i18n/resources-harness.ts @@ -0,0 +1,50 @@ +import { HarnessPredicate } from '@angular/cdk/testing'; +import { SkyComponentHarness } from '@skyux/core/testing'; + +import { SkyResourcesHarnessFilters } from './resources-harness-filters'; + +/** + * Harness for interacting with elements that display localized resource strings in tests. + * This harness can be used to verify that components correctly display localized content. + */ +export class SkyResourcesHarness extends SkyComponentHarness { + /** + * @internal + */ + public static hostSelector = '[skyLibResources],[skyAppResources]'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a + * `SkyResourcesHarness` that meets certain criteria. + */ + public static with( + filters: SkyResourcesHarnessFilters, + ): HarnessPredicate { + return SkyResourcesHarness.getDataSkyIdPredicate(filters); + } + + /** + * Gets the text content of the element. + */ + public async getText(): Promise { + const host = await this.host(); + return (await host.text()).trim(); + } + + /** + * Gets the inner HTML of the element. + */ + public async getInnerHtml(): Promise { + const host = await this.host(); + return await host.getProperty('innerHTML'); + } + + /** + * Gets the value of a specific attribute on the element. + * @param name The name of the attribute. + */ + public async getAttribute(name: string): Promise { + const host = await this.host(); + return await host.getAttribute(name); + } +} diff --git a/libs/components/i18n/testing/src/public-api.ts b/libs/components/i18n/testing/src/public-api.ts new file mode 100644 index 0000000000..513ae374ec --- /dev/null +++ b/libs/components/i18n/testing/src/public-api.ts @@ -0,0 +1,2 @@ +export { SkyResourcesHarness } from './modules/i18n/resources-harness'; +export { SkyResourcesHarnessFilters } from './modules/i18n/resources-harness-filters'; diff --git a/libs/components/i18n/testing/tsconfig.spec.json b/libs/components/i18n/testing/tsconfig.spec.json new file mode 100644 index 0000000000..aad2a9f430 --- /dev/null +++ b/libs/components/i18n/testing/tsconfig.spec.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.spec.json", + "compilerOptions": { + "outDir": "../../../../out-tsc/spec", + "types": ["jasmine", "node"] + }, + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/components/theme/testing/eslint.config.js b/libs/components/theme/testing/eslint.config.js new file mode 100644 index 0000000000..b4c331fb6a --- /dev/null +++ b/libs/components/theme/testing/eslint.config.js @@ -0,0 +1,5 @@ +const prettier = require('eslint-config-prettier'); +const baseConfig = require('../../../../eslint-base.config'); +const overrides = require('../../../../eslint-overrides.config'); + +module.exports = [...baseConfig, ...overrides, prettier]; diff --git a/libs/components/theme/testing/karma.conf.js b/libs/components/theme/testing/karma.conf.js new file mode 100644 index 0000000000..9ef51bf340 --- /dev/null +++ b/libs/components/theme/testing/karma.conf.js @@ -0,0 +1,19 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +const { join } = require('path'); +const getBaseKarmaConfig = require('../../../../karma.conf'); + +module.exports = function (config) { + const baseConfig = getBaseKarmaConfig(); + config.set({ + ...baseConfig, + coverageReporter: { + ...baseConfig.coverageReporter, + dir: join( + __dirname, + '../../../../coverage/libs/components/theme/testing', + ), + }, + }); +}; diff --git a/libs/components/theme/testing/ng-package.json b/libs/components/theme/testing/ng-package.json new file mode 100644 index 0000000000..fbafcc4448 --- /dev/null +++ b/libs/components/theme/testing/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/libs/components/theme/testing/project.json b/libs/components/theme/testing/project.json new file mode 100644 index 0000000000..c647194960 --- /dev/null +++ b/libs/components/theme/testing/project.json @@ -0,0 +1,47 @@ +{ + "name": "theme-testing", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "libs/components/theme/testing/src", + "prefix": "sky", + "tags": ["testing"], + "targets": { + "test": { + "executor": "@angular-devkit/build-angular:karma", + "options": { + "tsConfig": "libs/components/theme/testing/tsconfig.spec.json", + "karmaConfig": "libs/components/theme/testing/karma.conf.js", + "codeCoverage": true, + "codeCoverageExclude": ["**/fixtures/**"], + "styles": [ + "libs/components/theme/src/lib/styles/sky.scss", + "libs/components/theme/src/lib/styles/themes/modern/styles.scss" + ], + "polyfills": [ + "zone.js", + "zone.js/testing", + "libs/components/packages/src/polyfills.js" + ], + "inlineStyleLanguage": "scss", + "stylePreprocessorOptions": { + "includePaths": ["{workspaceRoot}"] + } + }, + "configurations": { + "ci": { + "browsers": "ChromeHeadlessNoSandbox", + "codeCoverage": true, + "progress": false, + "sourceMap": true, + "watch": false + } + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "options": { + "lintFilePatterns": ["{projectRoot}/src/**/*.ts"] + } + } + } +} diff --git a/libs/components/theme/testing/src/modules/theme/theme-harness-filters.ts b/libs/components/theme/testing/src/modules/theme/theme-harness-filters.ts new file mode 100644 index 0000000000..23371b4f78 --- /dev/null +++ b/libs/components/theme/testing/src/modules/theme/theme-harness-filters.ts @@ -0,0 +1,7 @@ +import { SkyHarnessFilters } from '@skyux/core/testing'; + +/** + * A set of criteria that can be used to filter a list of `SkyThemeHarness` instances. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type +export interface SkyThemeHarnessFilters extends SkyHarnessFilters {} diff --git a/libs/components/theme/testing/src/modules/theme/theme-harness.spec.ts b/libs/components/theme/testing/src/modules/theme/theme-harness.spec.ts new file mode 100644 index 0000000000..be14da12ee --- /dev/null +++ b/libs/components/theme/testing/src/modules/theme/theme-harness.spec.ts @@ -0,0 +1,226 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + SkyTheme, + SkyThemeMode, + SkyThemeModule, + SkyThemeSettings, + SkyThemeSpacing, +} from '@skyux/theme'; + +import { SkyThemeHarness } from './theme-harness'; + +@Component({ + selector: 'sky-theme-test', + template: `
+

Themed content

+
`, + standalone: false, +}) +class TestComponent { + public themeSettings: SkyThemeSettings | undefined; +} + +describe('Theme harness', () => { + async function setupTest(options: { dataSkyId?: string } = {}): Promise<{ + harness: SkyThemeHarness; + fixture: ComponentFixture; + loader: HarnessLoader; + }> { + await TestBed.configureTestingModule({ + declarations: [TestComponent], + imports: [SkyThemeModule], + }).compileComponents(); + + const fixture = TestBed.createComponent(TestComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + let harness: SkyThemeHarness; + + if (options.dataSkyId) { + harness = await loader.getHarness( + SkyThemeHarness.with({ + dataSkyId: options.dataSkyId, + }), + ); + } else { + harness = await loader.getHarness(SkyThemeHarness); + } + + return { harness, fixture, loader }; + } + + it('should return the default theme name when no theme is set', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'test-theme', + }); + + fixture.detectChanges(); + + await expectAsync(harness.getThemeName()).toBeResolvedTo('default'); + }); + + it('should return the default theme name when default theme is set', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'test-theme', + }); + + fixture.componentInstance.themeSettings = new SkyThemeSettings( + SkyTheme.presets.default, + SkyThemeMode.presets.light, + ); + fixture.detectChanges(); + + await expectAsync(harness.getThemeName()).toBeResolvedTo('default'); + }); + + it('should return the modern theme name when modern theme is set', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'test-theme', + }); + + fixture.componentInstance.themeSettings = new SkyThemeSettings( + SkyTheme.presets.modern, + SkyThemeMode.presets.light, + ); + fixture.detectChanges(); + + await expectAsync(harness.getThemeName()).toBeResolvedTo('modern'); + }); + + it('should return the light theme mode by default', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'test-theme', + }); + + fixture.detectChanges(); + + await expectAsync(harness.getThemeMode()).toBeResolvedTo('light'); + }); + + it('should return the light theme mode when light mode is set', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'test-theme', + }); + + fixture.componentInstance.themeSettings = new SkyThemeSettings( + SkyTheme.presets.modern, + SkyThemeMode.presets.light, + ); + fixture.detectChanges(); + + await expectAsync(harness.getThemeMode()).toBeResolvedTo('light'); + }); + + it('should return the dark theme mode when dark mode is set', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'test-theme', + }); + + fixture.componentInstance.themeSettings = new SkyThemeSettings( + SkyTheme.presets.modern, + SkyThemeMode.presets.dark, + ); + fixture.detectChanges(); + + await expectAsync(harness.getThemeMode()).toBeResolvedTo('dark'); + }); + + it('should return the standard theme spacing by default', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'test-theme', + }); + + fixture.detectChanges(); + + await expectAsync(harness.getThemeSpacing()).toBeResolvedTo('standard'); + }); + + it('should return the compact theme spacing when compact spacing is set', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'test-theme', + }); + + fixture.componentInstance.themeSettings = new SkyThemeSettings( + SkyTheme.presets.modern, + SkyThemeMode.presets.light, + SkyThemeSpacing.presets.compact, + ); + fixture.detectChanges(); + + await expectAsync(harness.getThemeSpacing()).toBeResolvedTo('compact'); + }); + + it('should return true for isModernTheme when modern theme is set', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'test-theme', + }); + + fixture.componentInstance.themeSettings = new SkyThemeSettings( + SkyTheme.presets.modern, + SkyThemeMode.presets.light, + ); + fixture.detectChanges(); + + await expectAsync(harness.isModernTheme()).toBeResolvedTo(true); + await expectAsync(harness.isDefaultTheme()).toBeResolvedTo(false); + }); + + it('should return true for isDefaultTheme when default theme is set', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'test-theme', + }); + + fixture.componentInstance.themeSettings = new SkyThemeSettings( + SkyTheme.presets.default, + SkyThemeMode.presets.light, + ); + fixture.detectChanges(); + + await expectAsync(harness.isDefaultTheme()).toBeResolvedTo(true); + await expectAsync(harness.isModernTheme()).toBeResolvedTo(false); + }); + + it('should return true for isDarkMode when dark mode is set', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'test-theme', + }); + + fixture.componentInstance.themeSettings = new SkyThemeSettings( + SkyTheme.presets.modern, + SkyThemeMode.presets.dark, + ); + fixture.detectChanges(); + + await expectAsync(harness.isDarkMode()).toBeResolvedTo(true); + await expectAsync(harness.isLightMode()).toBeResolvedTo(false); + }); + + it('should return true for isLightMode when light mode is set', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'test-theme', + }); + + fixture.componentInstance.themeSettings = new SkyThemeSettings( + SkyTheme.presets.modern, + SkyThemeMode.presets.light, + ); + fixture.detectChanges(); + + await expectAsync(harness.isLightMode()).toBeResolvedTo(true); + await expectAsync(harness.isDarkMode()).toBeResolvedTo(false); + }); + + it('should find harness without dataSkyId filter', async () => { + const { harness, fixture } = await setupTest(); + + fixture.detectChanges(); + + await expectAsync(harness.getThemeName()).toBeResolvedTo('default'); + }); +}); diff --git a/libs/components/theme/testing/src/modules/theme/theme-harness.ts b/libs/components/theme/testing/src/modules/theme/theme-harness.ts new file mode 100644 index 0000000000..0f5088a4cd --- /dev/null +++ b/libs/components/theme/testing/src/modules/theme/theme-harness.ts @@ -0,0 +1,94 @@ +import { HarnessPredicate } from '@angular/cdk/testing'; +import { SkyComponentHarness } from '@skyux/core/testing'; + +import { SkyThemeHarnessFilters } from './theme-harness-filters'; + +/** + * Harness for interacting with a theme directive in tests. + */ +export class SkyThemeHarness extends SkyComponentHarness { + /** + * @internal + */ + public static hostSelector = '[skyTheme]'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a + * `SkyThemeHarness` that meets certain criteria. + */ + public static with( + filters: SkyThemeHarnessFilters, + ): HarnessPredicate { + return SkyThemeHarness.getDataSkyIdPredicate(filters); + } + + /** + * Gets the current theme name. + * @returns The theme name ('default' or 'modern'). + */ + public async getThemeName(): Promise { + const host = await this.host(); + + if (await host.hasClass('sky-theme-modern')) { + return 'modern'; + } + + return 'default'; + } + + /** + * Gets the current theme mode. + * @returns The theme mode ('light' or 'dark'). + */ + public async getThemeMode(): Promise { + const host = await this.host(); + + if (await host.hasClass('sky-theme-mode-dark')) { + return 'dark'; + } + + return 'light'; + } + + /** + * Gets the current theme spacing. + * @returns The theme spacing ('standard' or 'compact'). + */ + public async getThemeSpacing(): Promise { + const host = await this.host(); + + if (await host.hasClass('sky-theme-modern-compact')) { + return 'compact'; + } + + return 'standard'; + } + + /** + * Gets whether the theme is the modern theme. + */ + public async isModernTheme(): Promise { + return (await this.getThemeName()) === 'modern'; + } + + /** + * Gets whether the theme is the default theme. + */ + public async isDefaultTheme(): Promise { + return (await this.getThemeName()) === 'default'; + } + + /** + * Gets whether the theme mode is dark. + */ + public async isDarkMode(): Promise { + return (await this.getThemeMode()) === 'dark'; + } + + /** + * Gets whether the theme mode is light. + */ + public async isLightMode(): Promise { + return (await this.getThemeMode()) === 'light'; + } +} diff --git a/libs/components/theme/testing/src/public-api.ts b/libs/components/theme/testing/src/public-api.ts new file mode 100644 index 0000000000..808caf99d4 --- /dev/null +++ b/libs/components/theme/testing/src/public-api.ts @@ -0,0 +1,2 @@ +export { SkyThemeHarness } from './modules/theme/theme-harness'; +export { SkyThemeHarnessFilters } from './modules/theme/theme-harness-filters'; diff --git a/libs/components/theme/testing/tsconfig.spec.json b/libs/components/theme/testing/tsconfig.spec.json new file mode 100644 index 0000000000..aad2a9f430 --- /dev/null +++ b/libs/components/theme/testing/tsconfig.spec.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.spec.json", + "compilerOptions": { + "outDir": "../../../../out-tsc/spec", + "types": ["jasmine", "node"] + }, + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index d382b60fcd..35799d75f8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -94,6 +94,9 @@ "libs/components/help-inline/testing/src/public-api.ts" ], "@skyux/i18n": ["libs/components/i18n/src/index.ts"], + "@skyux/i18n/testing": [ + "libs/components/i18n/testing/src/public-api.ts" + ], "@skyux/icon": ["libs/components/icon/src/index.ts"], "@skyux/icon/testing": ["libs/components/icon/testing/src/public-api.ts"], "@skyux/indicators": ["libs/components/indicators/src/index.ts"], @@ -184,6 +187,9 @@ "@skyux/tabs/testing": ["libs/components/tabs/testing/src/public-api.ts"], "@skyux/text-editor": ["libs/components/text-editor/src/index.ts"], "@skyux/theme": ["libs/components/theme/src/index.ts"], + "@skyux/theme/testing": [ + "libs/components/theme/testing/src/public-api.ts" + ], "@skyux/tiles": ["libs/components/tiles/src/index.ts"], "@skyux/tiles/testing": [ "libs/components/tiles/testing/src/public-api.ts" From 01e5051ba914b1dd9dfccee7241b84d56a3ef37d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:01:49 +0000 Subject: [PATCH 2/5] fix: address CI failures for formatting and peer dependencies - Fix formatting issues in test files and tsconfig.base.json - Add @angular/cdk and @skyux/core as peer dependencies to i18n and theme libraries Co-Authored-By: benc@cognition.ai --- libs/components/i18n/package.json | 4 +++- libs/components/i18n/testing/karma.conf.js | 5 +---- .../src/modules/i18n/resources-harness.spec.ts | 13 ++++++------- libs/components/theme/package.json | 4 +++- .../testing/src/modules/theme/theme-harness.spec.ts | 5 +---- tsconfig.base.json | 4 +--- 6 files changed, 15 insertions(+), 20 deletions(-) diff --git a/libs/components/i18n/package.json b/libs/components/i18n/package.json index a508f03802..7498d6167d 100644 --- a/libs/components/i18n/package.json +++ b/libs/components/i18n/package.json @@ -20,10 +20,12 @@ "save": "dependencies" }, "peerDependencies": { + "@angular/cdk": "^20.2.3", "@angular/cli": "^20.3.1", "@angular/common": "^20.3.0", "@angular/core": "^20.3.0", - "@skyux/assets": "0.0.0-PLACEHOLDER" + "@skyux/assets": "0.0.0-PLACEHOLDER", + "@skyux/core": "0.0.0-PLACEHOLDER" }, "dependencies": { "tslib": "^2.8.1" diff --git a/libs/components/i18n/testing/karma.conf.js b/libs/components/i18n/testing/karma.conf.js index 20953f5107..2146dcca8a 100644 --- a/libs/components/i18n/testing/karma.conf.js +++ b/libs/components/i18n/testing/karma.conf.js @@ -10,10 +10,7 @@ module.exports = function (config) { ...baseConfig, coverageReporter: { ...baseConfig.coverageReporter, - dir: join( - __dirname, - '../../../../coverage/libs/components/i18n/testing', - ), + dir: join(__dirname, '../../../../coverage/libs/components/i18n/testing'), }, }); }; diff --git a/libs/components/i18n/testing/src/modules/i18n/resources-harness.spec.ts b/libs/components/i18n/testing/src/modules/i18n/resources-harness.spec.ts index a06750647d..ec57f2d910 100644 --- a/libs/components/i18n/testing/src/modules/i18n/resources-harness.spec.ts +++ b/libs/components/i18n/testing/src/modules/i18n/resources-harness.spec.ts @@ -9,14 +9,13 @@ import { SkyResourcesHarness } from './resources-harness'; @Component({ selector: 'sky-resources-test', template: ` + {{ resourceText }} {{ resourceText }} - {{ - resourceText - }} `, standalone: false, }) @@ -110,9 +109,9 @@ describe('Resources harness', () => { fixture.detectChanges(); - await expectAsync( - harness.getAttribute('non-existent-attr'), - ).toBeResolvedTo(null); + await expectAsync(harness.getAttribute('non-existent-attr')).toBeResolvedTo( + null, + ); }); it('should find harness without dataSkyId filter', async () => { diff --git a/libs/components/theme/package.json b/libs/components/theme/package.json index f5ab8571cf..68a2445ac1 100644 --- a/libs/components/theme/package.json +++ b/libs/components/theme/package.json @@ -16,8 +16,10 @@ }, "homepage": "https://github.com/blackbaud/skyux#readme", "peerDependencies": { + "@angular/cdk": "^20.2.3", "@angular/common": "^20.3.0", - "@angular/core": "^20.3.0" + "@angular/core": "^20.3.0", + "@skyux/core": "0.0.0-PLACEHOLDER" }, "dependencies": { "@blackbaud/skyux-design-tokens": "3.2.0", diff --git a/libs/components/theme/testing/src/modules/theme/theme-harness.spec.ts b/libs/components/theme/testing/src/modules/theme/theme-harness.spec.ts index be14da12ee..a0d240cd95 100644 --- a/libs/components/theme/testing/src/modules/theme/theme-harness.spec.ts +++ b/libs/components/theme/testing/src/modules/theme/theme-harness.spec.ts @@ -14,10 +14,7 @@ import { SkyThemeHarness } from './theme-harness'; @Component({ selector: 'sky-theme-test', - template: `
+ template: `

Themed content

`, standalone: false, diff --git a/tsconfig.base.json b/tsconfig.base.json index 35799d75f8..967f035085 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -94,9 +94,7 @@ "libs/components/help-inline/testing/src/public-api.ts" ], "@skyux/i18n": ["libs/components/i18n/src/index.ts"], - "@skyux/i18n/testing": [ - "libs/components/i18n/testing/src/public-api.ts" - ], + "@skyux/i18n/testing": ["libs/components/i18n/testing/src/public-api.ts"], "@skyux/icon": ["libs/components/icon/src/index.ts"], "@skyux/icon/testing": ["libs/components/icon/testing/src/public-api.ts"], "@skyux/indicators": ["libs/components/indicators/src/index.ts"], From dc69898d6509b98ae3d99fdeb78cf75361d972e5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:08:16 +0000 Subject: [PATCH 3/5] fix: resolve circular dependency by restructuring i18n harness - Remove @skyux/core from i18n peer dependencies to avoid circular dependency - Restructure i18n harness to extend ComponentHarness directly from @angular/cdk/testing - Create local implementation of dataSkyId filter to avoid importing from @skyux/core/testing Co-Authored-By: benc@cognition.ai --- libs/components/i18n/package.json | 3 +-- .../modules/i18n/resources-harness-filters.ts | 10 +++++++--- .../src/modules/i18n/resources-harness.ts | 16 ++++++++++++---- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/libs/components/i18n/package.json b/libs/components/i18n/package.json index 7498d6167d..b1dc472601 100644 --- a/libs/components/i18n/package.json +++ b/libs/components/i18n/package.json @@ -24,8 +24,7 @@ "@angular/cli": "^20.3.1", "@angular/common": "^20.3.0", "@angular/core": "^20.3.0", - "@skyux/assets": "0.0.0-PLACEHOLDER", - "@skyux/core": "0.0.0-PLACEHOLDER" + "@skyux/assets": "0.0.0-PLACEHOLDER" }, "dependencies": { "tslib": "^2.8.1" diff --git a/libs/components/i18n/testing/src/modules/i18n/resources-harness-filters.ts b/libs/components/i18n/testing/src/modules/i18n/resources-harness-filters.ts index d6a897e060..bef173540e 100644 --- a/libs/components/i18n/testing/src/modules/i18n/resources-harness-filters.ts +++ b/libs/components/i18n/testing/src/modules/i18n/resources-harness-filters.ts @@ -1,7 +1,11 @@ -import { SkyHarnessFilters } from '@skyux/core/testing'; +import { BaseHarnessFilters } from '@angular/cdk/testing'; /** * A set of criteria that can be used to filter a list of `SkyResourcesHarness` instances. */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type -export interface SkyResourcesHarnessFilters extends SkyHarnessFilters {} +export interface SkyResourcesHarnessFilters extends BaseHarnessFilters { + /** + * Only find instances whose `data-sky-id` attribute matches the given value. + */ + dataSkyId?: string; +} diff --git a/libs/components/i18n/testing/src/modules/i18n/resources-harness.ts b/libs/components/i18n/testing/src/modules/i18n/resources-harness.ts index bc0228ddb8..2551d27c8d 100644 --- a/libs/components/i18n/testing/src/modules/i18n/resources-harness.ts +++ b/libs/components/i18n/testing/src/modules/i18n/resources-harness.ts @@ -1,5 +1,4 @@ -import { HarnessPredicate } from '@angular/cdk/testing'; -import { SkyComponentHarness } from '@skyux/core/testing'; +import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing'; import { SkyResourcesHarnessFilters } from './resources-harness-filters'; @@ -7,7 +6,7 @@ import { SkyResourcesHarnessFilters } from './resources-harness-filters'; * Harness for interacting with elements that display localized resource strings in tests. * This harness can be used to verify that components correctly display localized content. */ -export class SkyResourcesHarness extends SkyComponentHarness { +export class SkyResourcesHarness extends ComponentHarness { /** * @internal */ @@ -20,7 +19,16 @@ export class SkyResourcesHarness extends SkyComponentHarness { public static with( filters: SkyResourcesHarnessFilters, ): HarnessPredicate { - return SkyResourcesHarness.getDataSkyIdPredicate(filters); + return new HarnessPredicate(SkyResourcesHarness, filters).addOption( + 'dataSkyId', + filters.dataSkyId, + (harness, text) => + HarnessPredicate.stringMatches(harness.#getSkyId(), text), + ); + } + + async #getSkyId(): Promise { + return await (await this.host()).getAttribute('data-sky-id'); } /** From 775970a823895e07526aef3879f1a933a2d0bc7d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:13:47 +0000 Subject: [PATCH 4/5] fix: resolve circular dependency by restructuring theme harness - Remove @skyux/core from theme peer dependencies to avoid circular dependency - Restructure theme harness to extend ComponentHarness directly from @angular/cdk/testing - Create local implementation of dataSkyId filter to avoid importing from @skyux/core/testing Co-Authored-By: benc@cognition.ai --- libs/components/theme/package.json | 3 +-- .../src/modules/theme/theme-harness-filters.ts | 10 +++++++--- .../testing/src/modules/theme/theme-harness.ts | 16 ++++++++++++---- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/libs/components/theme/package.json b/libs/components/theme/package.json index 68a2445ac1..df281c91fc 100644 --- a/libs/components/theme/package.json +++ b/libs/components/theme/package.json @@ -18,8 +18,7 @@ "peerDependencies": { "@angular/cdk": "^20.2.3", "@angular/common": "^20.3.0", - "@angular/core": "^20.3.0", - "@skyux/core": "0.0.0-PLACEHOLDER" + "@angular/core": "^20.3.0" }, "dependencies": { "@blackbaud/skyux-design-tokens": "3.2.0", diff --git a/libs/components/theme/testing/src/modules/theme/theme-harness-filters.ts b/libs/components/theme/testing/src/modules/theme/theme-harness-filters.ts index 23371b4f78..cf71b2e3c3 100644 --- a/libs/components/theme/testing/src/modules/theme/theme-harness-filters.ts +++ b/libs/components/theme/testing/src/modules/theme/theme-harness-filters.ts @@ -1,7 +1,11 @@ -import { SkyHarnessFilters } from '@skyux/core/testing'; +import { BaseHarnessFilters } from '@angular/cdk/testing'; /** * A set of criteria that can be used to filter a list of `SkyThemeHarness` instances. */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type -export interface SkyThemeHarnessFilters extends SkyHarnessFilters {} +export interface SkyThemeHarnessFilters extends BaseHarnessFilters { + /** + * Only find instances whose `data-sky-id` attribute matches the given value. + */ + dataSkyId?: string; +} diff --git a/libs/components/theme/testing/src/modules/theme/theme-harness.ts b/libs/components/theme/testing/src/modules/theme/theme-harness.ts index 0f5088a4cd..bac3f908f7 100644 --- a/libs/components/theme/testing/src/modules/theme/theme-harness.ts +++ b/libs/components/theme/testing/src/modules/theme/theme-harness.ts @@ -1,12 +1,11 @@ -import { HarnessPredicate } from '@angular/cdk/testing'; -import { SkyComponentHarness } from '@skyux/core/testing'; +import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing'; import { SkyThemeHarnessFilters } from './theme-harness-filters'; /** * Harness for interacting with a theme directive in tests. */ -export class SkyThemeHarness extends SkyComponentHarness { +export class SkyThemeHarness extends ComponentHarness { /** * @internal */ @@ -19,7 +18,16 @@ export class SkyThemeHarness extends SkyComponentHarness { public static with( filters: SkyThemeHarnessFilters, ): HarnessPredicate { - return SkyThemeHarness.getDataSkyIdPredicate(filters); + return new HarnessPredicate(SkyThemeHarness, filters).addOption( + 'dataSkyId', + filters.dataSkyId, + (harness, text) => + HarnessPredicate.stringMatches(harness.#getSkyId(), text), + ); + } + + async #getSkyId(): Promise { + return await (await this.host()).getAttribute('data-sky-id'); } /** From 14104aa95fa3ec0f72fa6eb4e269c28f7b00832a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:18:19 +0000 Subject: [PATCH 5/5] chore: update library dependencies for testing packages Co-Authored-By: benc@cognition.ai --- libs/components/i18n/project.json | 14 +++++++++++--- libs/components/theme/project.json | 14 +++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/libs/components/i18n/project.json b/libs/components/i18n/project.json index a30d8876c5..11e54cb8fb 100644 --- a/libs/components/i18n/project.json +++ b/libs/components/i18n/project.json @@ -4,6 +4,7 @@ "projectType": "library", "sourceRoot": "libs/components/i18n/src", "prefix": "sky", + "tags": ["component", "npm"], "targets": { "build": { "executor": "@nx/angular:package", @@ -19,7 +20,15 @@ "tsConfig": "libs/components/i18n/tsconfig.lib.json" } }, - "defaultConfiguration": "production" + "defaultConfiguration": "production", + "dependsOn": ["^build"], + "inputs": [ + "buildInputs", + "^buildInputs", + "{workspaceRoot}/libs/components/i18n/testing/src/**/*", + "!{workspaceRoot}/libs/components/i18n/testing/src/**/*.spec.ts", + "!{workspaceRoot}/libs/components/i18n/testing/src/**/fixtures/**/*" + ] }, "test": { "executor": "@angular-devkit/build-angular:karma", @@ -74,6 +83,5 @@ "command": "ts-node --project ./scripts/tsconfig.json ./scripts/posttest-i18n.ts" } } - }, - "tags": ["component", "npm"] + } } diff --git a/libs/components/theme/project.json b/libs/components/theme/project.json index 54c38b1af1..cd51ebf3ec 100644 --- a/libs/components/theme/project.json +++ b/libs/components/theme/project.json @@ -4,6 +4,7 @@ "projectType": "library", "sourceRoot": "libs/components/theme/src", "prefix": "sky", + "tags": ["component", "npm"], "targets": { "build": { "executor": "@nx/angular:package", @@ -19,7 +20,15 @@ "tsConfig": "libs/components/theme/tsconfig.lib.json" } }, - "defaultConfiguration": "production" + "defaultConfiguration": "production", + "dependsOn": ["^build"], + "inputs": [ + "buildInputs", + "^buildInputs", + "{workspaceRoot}/libs/components/theme/testing/src/**/*", + "!{workspaceRoot}/libs/components/theme/testing/src/**/*.spec.ts", + "!{workspaceRoot}/libs/components/theme/testing/src/**/fixtures/**/*" + ] }, "test": { "executor": "@angular-devkit/build-angular:karma", @@ -67,6 +76,5 @@ "command": "ts-node --project ./scripts/tsconfig.json ./scripts/postbuild-theme.ts" } } - }, - "tags": ["component", "npm"] + } }