diff --git a/src/shared/declaration/__tests__/index.test.ts b/src/shared/declaration/__tests__/index.test.ts index 71c33bb..19b0f34 100644 --- a/src/shared/declaration/__tests__/index.test.ts +++ b/src/shared/declaration/__tests__/index.test.ts @@ -117,4 +117,75 @@ describe('renderDeclarations', () => { expect(output).toContain('.dark.secondary-theme .navigation,.dark.navigation.secondary-theme'); }); + + test('context token that differs from base is present in the same-element context rule', () => { + // shadow is overridden in navigationContext. The same-element context rule + // (.navigation.secondary-theme) must contain it, not just the descendant form. + // If both rules have identical content they will be merged into a comma-selector. + const output = createBuildDeclarations( + secondaryTheme, + [], + preset.propertiesMap, + (selector) => selector, + Object.keys(secondaryTheme.tokens), + ); + + const mergedRule = output.match(/\.secondary-theme \.navigation,\.navigation\.secondary-theme\s*\{([^}]*)\}/); + expect(mergedRule).not.toBeNull(); + expect(mergedRule![1]).toContain('--shadow-css'); + }); + + test('context token that differs from base is present in the same-element context rule (multi-theme path)', () => { + // Same as above but via MultiThemeCreator (primary + secondary). + // The merged comma-selector proves both rules have identical content — if the + // same-element rule were diffed against a different parent, it would contain + // extra tokens and the rules would not merge. + // boxShadow is set in the primary's navigationContext but not in the secondary's, + // so it must be present in both the descendant and same-element secondary context rules. + const output = createBuildDeclarations( + rootTheme, + [secondaryTheme], + preset.propertiesMap, + (selector) => selector, + Object.keys(rootTheme.tokens), + ); + + const mergedRule = output.match(/\.secondary-theme \.navigation,\.navigation\.secondary-theme\s*\{([^}]*)\}/); + expect(mergedRule).not.toBeNull(); + expect(mergedRule![1]).toContain('--boxShadow-css'); + }); + + test('mode+context descendant and same-element rules have identical declarations and are merged', () => { + // Both selectors must resolve to the same declarations so mergeSelectors can combine them. + const output = createBuildDeclarations( + secondaryTheme, + [], + preset.propertiesMap, + (selector) => selector, + Object.keys(secondaryTheme.tokens), + ); + + const mergedRule = output.match( + /\.dark\.secondary-theme \.navigation,\.dark\.navigation\.secondary-theme\s*\{([^}]*)\}/, + ); + expect(mergedRule).not.toBeNull(); + expect(mergedRule![1]).toContain('--shadow-css'); + }); + + test('mode+context descendant and same-element rules have identical declarations and are merged (multi-theme path)', () => { + // Same as above but via MultiThemeCreator (primary + secondary). + const output = createBuildDeclarations( + rootTheme, + [secondaryTheme], + preset.propertiesMap, + (selector) => selector, + Object.keys(rootTheme.tokens), + ); + + const mergedRule = output.match( + /\.dark\.secondary-theme \.navigation,\.dark\.navigation\.secondary-theme\s*\{([^}]*)\}/, + ); + expect(mergedRule).not.toBeNull(); + expect(mergedRule![1]).toContain('--shadow-css'); + }); }); diff --git a/src/shared/declaration/multi.ts b/src/shared/declaration/multi.ts index d82c622..5195ab7 100644 --- a/src/shared/declaration/multi.ts +++ b/src/shared/declaration/multi.ts @@ -114,7 +114,7 @@ export class MultiThemeCreator extends AbstractCreator implements StylesheetCrea MultiThemeCreator.appendRuleToStylesheet( stylesheet, contextRuleGlobal, - compact([parentContextRule, rootRule, parentRule]), + compact([rootRule, parentContextRule, parentRule]), ); }); @@ -126,6 +126,7 @@ export class MultiThemeCreator extends AbstractCreator implements StylesheetCrea modeReducer(mode, state), ); const contextRule = this.findRule(stylesheet, { global: [secondary.selector], local: [context.selector] }); + const contextRuleGlobal = this.findRule(stylesheet, { global: [secondary.selector, context.selector] }); const modeRule = this.findRule(stylesheet, { global: [secondary.selector, optionalState.selector], }); @@ -179,7 +180,7 @@ export class MultiThemeCreator extends AbstractCreator implements StylesheetCrea stylesheet, contextAndModeRuleGlobal, compact([ - contextRule, + contextRuleGlobal, parentContextAndModeRule, parentContextRule, modeRule, diff --git a/src/shared/declaration/single.ts b/src/shared/declaration/single.ts index 27b8c81..9a0dd03 100644 --- a/src/shared/declaration/single.ts +++ b/src/shared/declaration/single.ts @@ -86,6 +86,9 @@ export class SingleThemeCreator extends AbstractCreator implements StylesheetCre const contextRule = stylesheet.findRule( this.ruleCreator.selectorFor({ global: [this.theme.selector], local: [context.selector] }), ); + const contextRuleGlobal = stylesheet.findRule( + this.ruleCreator.selectorFor({ global: [this.theme.selector, context.selector] }), + ); const modeRule = stylesheet.findRule( this.ruleCreator.selectorFor({ global: [this.theme.selector, (mode.states[state] as OptionalState).selector], @@ -105,7 +108,7 @@ export class SingleThemeCreator extends AbstractCreator implements StylesheetCre SingleThemeCreator.appendRuleToStylesheet( stylesheet, contextRuleAndModeRuleGlobal, - compact([contextRule, modeRule, rootRule]), + compact([contextRuleGlobal, modeRule, rootRule]), ); });