From 61240ac90327ec94a9f5b91aa2e7eedbaca6a7b2 Mon Sep 17 00:00:00 2001 From: Jonathan Haines Date: Wed, 9 Oct 2024 14:56:50 +1100 Subject: [PATCH] feature: use `Element.computedStyleMap()` --- packages/qunit-dom/lib/assertions.ts | 27 ++---- .../lib/helpers/get-computed-style.test.ts | 92 +++++++++++++++++++ .../lib/helpers/get-computed-style.ts | 57 ++++++++++++ packages/qunit-dom/package.json | 3 +- pnpm-lock.yaml | 7 ++ 5 files changed, 167 insertions(+), 19 deletions(-) create mode 100644 packages/qunit-dom/lib/helpers/get-computed-style.test.ts create mode 100644 packages/qunit-dom/lib/helpers/get-computed-style.ts diff --git a/packages/qunit-dom/lib/assertions.ts b/packages/qunit-dom/lib/assertions.ts index e3e26d631..89efe6f0b 100644 --- a/packages/qunit-dom/lib/assertions.ts +++ b/packages/qunit-dom/lib/assertions.ts @@ -10,6 +10,7 @@ import isVisible from './assertions/is-visible.js'; import isDisabled from './assertions/is-disabled.js'; import matchesSelector from './assertions/matches-selector.js'; import collapseWhitespace from './helpers/collapse-whitespace.js'; +import getComputedStyle from './helpers/get-computed-style.js'; import { type IDOMElementDescriptor, resolveDOMElement, @@ -29,10 +30,6 @@ export interface ExistsOptions { count: number; } -type CSSStyleDeclarationProperty = keyof CSSStyleDeclaration; - -type ActualCSSStyleDeclaration = Partial>; - /** * @namespace */ @@ -832,8 +829,8 @@ export default class DOMAssertions { let element = this.findTargetElement(); if (!element) return this; - let computedStyle = window.getComputedStyle(element, selector); - let expectedProperties = Object.keys(expected) as CSSStyleDeclarationProperty[]; + let computedStyle = getComputedStyle(element, selector); + let expectedProperties = Object.keys(expected); if (expectedProperties.length <= 0) { throw new TypeError( `Missing style expectations. There must be at least one style property in the passed in expectation object.` @@ -841,17 +838,11 @@ export default class DOMAssertions { } let result = expectedProperties.every( - property => - (computedStyle.getPropertyValue(property.toString()) || computedStyle[property]) === - expected[property] + property => computedStyle[property] === expected[property] ); - let actual: ActualCSSStyleDeclaration = {}; + let actual: Record = {}; - expectedProperties.forEach( - property => - (actual[property] = - computedStyle.getPropertyValue(property.toString()) || computedStyle[property]) - ); + expectedProperties.forEach(property => (actual[property] = computedStyle[property])); if (!message) { let normalizedSelector = selector ? selector.replace(/^:{0,2}/, '::') : ''; @@ -912,9 +903,9 @@ export default class DOMAssertions { let element = this.findTargetElement(); if (!element) return this; - let computedStyle = window.getComputedStyle(element, selector); + let computedStyle = getComputedStyle(element, selector); - let expectedProperties = Object.keys(expected) as CSSStyleDeclarationProperty[]; + let expectedProperties = Object.keys(expected); if (expectedProperties.length <= 0) { throw new TypeError( `Missing style expectations. There must be at least one style property in the passed in expectation object.` @@ -924,7 +915,7 @@ export default class DOMAssertions { let result = expectedProperties.some( property => computedStyle[property] !== expected[property] ); - let actual: ActualCSSStyleDeclaration = {}; + let actual: Record = {}; expectedProperties.forEach(property => (actual[property] = computedStyle[property])); diff --git a/packages/qunit-dom/lib/helpers/get-computed-style.test.ts b/packages/qunit-dom/lib/helpers/get-computed-style.test.ts new file mode 100644 index 000000000..37f0537dc --- /dev/null +++ b/packages/qunit-dom/lib/helpers/get-computed-style.test.ts @@ -0,0 +1,92 @@ +import { describe, beforeEach, test, expect, vi } from 'vitest'; + +import getComputedStyle from './get-computed-style'; + +describe('getComputedStyle', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + test('when computedStyleMap() is supported', () => { + Element.prototype.computedStyleMap = vi.fn().mockReturnValue(new Map([['width', '100px']])); + + const element = document.createElement('div'); + document.body.appendChild(element); + + const computedStyle = getComputedStyle(element); + expect(computedStyle).toHaveProperty('width', '100px'); + }); + + test('when computedStyleMap() is not supported', () => { + const element = document.createElement('div'); + element.style.width = '100px'; + document.body.appendChild(element); + + const computedStyle = getComputedStyle(element); + expect(computedStyle).toHaveProperty('width', '100px'); + }); + + test('kebab-case properties', () => { + const element = document.createElement('div'); + element.style.textAlign = 'center'; + document.body.appendChild(element); + + const computedStyle = getComputedStyle(element); + expect(computedStyle).toHaveProperty('text-align', 'center'); + }); + + test('camelCase properties', () => { + const element = document.createElement('div'); + element.style.textAlign = 'center'; + document.body.appendChild(element); + + const computedStyle = getComputedStyle(element); + expect(computedStyle).toHaveProperty('textAlign', 'center'); + }); + + test('iterating over StylePropertyMap properties', () => { + Element.prototype.computedStyleMap = vi.fn().mockReturnValue( + new Map([ + ['height', '200px'], + ['width', '100px'], + ]) + ); + + const element = document.createElement('div'); + document.body.appendChild(element); + + const computedStyle = getComputedStyle(element); + expect(Object.keys(computedStyle)).toEqual(['height', 'width']); + }); + + test('iterating over CSSStyleDeclaration properties', () => { + const element = document.createElement('div'); + element.style.height = '200px'; + element.style.width = '100px'; + document.body.appendChild(element); + + const computedStyle = getComputedStyle(element); + expect(Object.keys(computedStyle)).toEqual(expect.arrayContaining(['0', '1', '2', '3'])); + }); + + test('the existence of a StylePropertyMap property', () => { + const element = document.createElement('div'); + element.style.textAlign = '200px'; + document.body.appendChild(element); + + const computedStyle = getComputedStyle(element); + expect(Reflect.has(computedStyle, 'text-align')).toBe(true); + }); + + test('the existence of a CSSStyleDeclaration property', () => { + Element.prototype.computedStyleMap = vi + .fn() + .mockReturnValue(new Map([['text-align', 'center']])); + + const element = document.createElement('div'); + document.body.appendChild(element); + + const computedStyle = getComputedStyle(element); + expect(Reflect.has(computedStyle, 'text-align')).toBe(true); + }); +}); diff --git a/packages/qunit-dom/lib/helpers/get-computed-style.ts b/packages/qunit-dom/lib/helpers/get-computed-style.ts new file mode 100644 index 000000000..78f0fb196 --- /dev/null +++ b/packages/qunit-dom/lib/helpers/get-computed-style.ts @@ -0,0 +1,57 @@ +export default function getComputedStyle(element: Element, selector?: string | null) { + let computedStyleMap: StylePropertyMapReadOnly | undefined; + + if (!selector && 'computedStyleMap' in element) { + computedStyleMap = element.computedStyleMap(); + } + + const computedStyle = window.getComputedStyle(element, selector); + + return new Proxy>( + {}, + { + get(_, property) { + let value; + + if (typeof property === 'string' && computedStyleMap) { + value = computedStyleMap.get(property)?.toString(); + } + + return ( + value || + computedStyle.getPropertyValue(property.toString()) || + Reflect.get(computedStyle, property) + ); + }, + + has(_, property) { + if (computedStyleMap && typeof property === 'string') { + return computedStyleMap.has(property); + } + + return Reflect.has(computedStyle, property); + }, + + ownKeys(_) { + if (computedStyleMap) { + return Array.from(computedStyleMap.keys()); + } + + return Reflect.ownKeys(computedStyle); + }, + + getOwnPropertyDescriptor(_, property) { + if (computedStyleMap && typeof property == 'string') { + return { + writable: false, + configurable: true, + enumerable: true, + value: computedStyleMap.get(property), + }; + } + + return Reflect.getOwnPropertyDescriptor(computedStyle, property); + }, + } + ); +} diff --git a/packages/qunit-dom/package.json b/packages/qunit-dom/package.json index d169a8bd0..070dba4e1 100644 --- a/packages/qunit-dom/package.json +++ b/packages/qunit-dom/package.json @@ -41,6 +41,7 @@ "devDependencies": { "@arethetypeswrong/cli": "0.15.4", "@types/qunit": "2.19.10", + "@typescript/lib-dom": "npm:@types/web@0.0.169", "@typescript-eslint/eslint-plugin": "7.18.0", "@typescript-eslint/parser": "7.18.0", "@vitest/coverage-v8": "2.0.5", @@ -71,4 +72,4 @@ "volta": { "extends": "../../package.json" } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00f595a86..d319b6752 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,9 @@ importers: '@typescript-eslint/parser': specifier: 7.18.0 version: 7.18.0(eslint@8.57.1)(typescript@5.5.4) + '@typescript/lib-dom': + specifier: npm:@types/web@0.0.169 + version: /@types/web@0.0.169 '@vitest/coverage-v8': specifier: 2.0.5 version: 2.0.5(vitest@2.0.5) @@ -5091,6 +5094,10 @@ packages: resolution: {integrity: sha512-d0XxK3YTObnWVp6rZuev3c49+j4Lo8g4L1ZRm9z5L0xpoZycUPshHgczK5gsUMaZOstjVYYi09p5gYvUtfChYw==} dev: true + /@types/web@0.0.169: + resolution: {integrity: sha512-oRSjHjC3f3/e/8b6jpPfjDnQxJR9s6ajeHJTaN6fnjg466wDO/inKStV52uplNraDg5MJ3sFTYLoBJIdR0w2sg==} + dev: true + /@types/yargs-parser@21.0.1: resolution: {integrity: sha512-axdPBuLuEJt0c4yI5OZssC19K2Mq1uKdrfZBzuxLvaztgqUtFYZUNw7lETExPYJR9jdEoIg4mb7RQKRQzOkeGQ==} dev: true