From f8d9a1a761460ccb19919c9109cd8ce3f5c688a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 09:15:00 +0000 Subject: [PATCH 1/2] test(styled-tools): add unit tests Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01DXXPnGepYUGie3we1X9PTR --- packages/styled-tools/package.json | 5 + packages/styled-tools/src/lib/mkProxy.spec.ts | 110 ++++++++++++++++++ .../styled-tools/src/lib/mkTheme.spec.tsx | 97 +++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 packages/styled-tools/src/lib/mkProxy.spec.ts create mode 100644 packages/styled-tools/src/lib/mkTheme.spec.tsx diff --git a/packages/styled-tools/package.json b/packages/styled-tools/package.json index 723f54c27..7501aaa25 100644 --- a/packages/styled-tools/package.json +++ b/packages/styled-tools/package.json @@ -17,6 +17,11 @@ "peerDependencies": { "styled-components": ">=6.0.0" }, + "devDependencies": { + "@testing-library/react": "*", + "react": "*", + "vitest": "*" + }, "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" diff --git a/packages/styled-tools/src/lib/mkProxy.spec.ts b/packages/styled-tools/src/lib/mkProxy.spec.ts new file mode 100644 index 000000000..354c82803 --- /dev/null +++ b/packages/styled-tools/src/lib/mkProxy.spec.ts @@ -0,0 +1,110 @@ +import type { ExecutionContext } from "styled-components" +import { describe, expect, it } from "vitest" +import { mkProxy, Value } from "./mkProxy" + +// Builds a minimal ExecutionContext carrying the provided theme value. +function ctx(theme: Value): ExecutionContext { + return { theme } as unknown as ExecutionContext +} + +describe("mkProxy", () => { + it("returns the accessor result when invoked directly with a context", () => { + const proxy = mkProxy(c => c.theme as Value) + + expect((proxy as unknown as (c: ExecutionContext) => Value)(ctx("root-value"))).toBe("root-value") + }) + + it("traverses nested object properties lazily until invoked", () => { + const theme = { colors: { primary: "#fff", secondary: "#000" } } + const proxy = mkProxy(c => c.theme as Value) as any + + expect(proxy.colors.primary(ctx(theme))).toBe("#fff") + expect(proxy.colors.secondary(ctx(theme))).toBe("#000") + }) + + it("resolves deeply nested values across multiple levels", () => { + const theme = { a: { b: { c: { d: 42 } } } } + const proxy = mkProxy(c => c.theme as Value) as any + + expect(proxy.a.b.c.d(ctx(theme))).toBe(42) + }) + + it("returns undefined when a property is accessed on a non-object (string) value", () => { + const theme = { colors: "not-an-object" } + const proxy = mkProxy(c => c.theme as Value) as any + + // colors resolves to a string; reaching into it must hit the guard and return undefined + expect(proxy.colors.primary(ctx(theme))).toBeUndefined() + }) + + it("returns undefined when a property is accessed on a number value", () => { + const theme = { spacing: 8 } + const proxy = mkProxy(c => c.theme as Value) as any + + expect(proxy.spacing.small(ctx(theme))).toBeUndefined() + }) + + it("returns undefined when the accessor itself yields undefined", () => { + const emptyValue: Value = undefined + const proxy = mkProxy(() => emptyValue) as any + + expect(proxy(ctx(emptyValue))).toBeUndefined() + expect(proxy.anything(ctx(emptyValue))).toBeUndefined() + }) + + it("returns undefined for a missing property on an object value", () => { + const theme = { colors: { primary: "#fff" } } + const proxy = mkProxy(c => c.theme as Value) as any + + expect(proxy.colors.missing(ctx(theme))).toBeUndefined() + }) + + it("exposes Function.prototype for the special 'prototype' property", () => { + const proxy = mkProxy(c => c.theme as Value) as any + + expect(proxy.prototype).toBe(Function.prototype) + }) + + it("caches nested proxies so repeated access returns the same reference", () => { + const proxy = mkProxy(c => c.theme as Value) as any + + const first = proxy.colors + const second = proxy.colors + + expect(first).toBe(second) + }) + + it("creates distinct proxies for distinct properties", () => { + const proxy = mkProxy(c => c.theme as Value) as any + + expect(proxy.colors).not.toBe(proxy.spacing) + }) + + it("re-evaluates the accessor against the context on every invocation", () => { + const proxy = mkProxy(c => c.theme as Value) as any + + const themeA = { colors: { primary: "#aaa" } } + const themeB = { colors: { primary: "#bbb" } } + + // Same cached proxy node, different contexts -> different resolved values + const accessor = proxy.colors.primary + expect(accessor(ctx(themeA))).toBe("#aaa") + expect(accessor(ctx(themeB))).toBe("#bbb") + }) + + it("supports symbol-keyed property access", () => { + const key = Symbol("token") + const theme = { [key]: "symbol-value" } + const proxy = mkProxy(c => c.theme as Value) as any + + expect(proxy[key](ctx(theme))).toBe("symbol-value") + }) + + it("treats null as an object access (typeof null === 'object') and returns undefined for its props", () => { + // guard() uses typeof === "object"; null passes the guard but optional chaining yields undefined + const theme = { colors: null as unknown as Value } + const proxy = mkProxy(c => c.theme as Value) as any + + expect(proxy.colors.primary(ctx(theme))).toBeUndefined() + }) +}) diff --git a/packages/styled-tools/src/lib/mkTheme.spec.tsx b/packages/styled-tools/src/lib/mkTheme.spec.tsx new file mode 100644 index 000000000..b83c82a7b --- /dev/null +++ b/packages/styled-tools/src/lib/mkTheme.spec.tsx @@ -0,0 +1,97 @@ +import { type ReactNode } from "react" +import { renderHook } from "@testing-library/react" +import { type ExecutionContext, ThemeProvider } from "styled-components" +import { describe, expect, it } from "vitest" +import { mkTheme } from "./mkTheme" + +type AppTheme = { + colors: { primary: string; secondary: string } + spacing: { small: string; large: string } +} + +const appTheme: AppTheme = { + colors: { primary: "#ff0000", secondary: "#00ff00" }, + spacing: { small: "4px", large: "16px" }, +} + +// Resolves a styled-components interpolation against an ExecutionContext carrying the theme. +function withTheme(theme: AppTheme): ExecutionContext { + return { theme } as unknown as ExecutionContext +} + +describe("mkTheme", () => { + it("returns an object exposing both a theme proxy and a useTheme hook", () => { + const tools = mkTheme() + + expect(tools).toHaveProperty("theme") + expect(tools).toHaveProperty("useTheme") + expect(typeof tools.useTheme).toBe("function") + }) + + describe("theme proxy", () => { + it("reads top-level nested values from the execution context theme", () => { + const { theme } = mkTheme() + + const primary = theme.colors.primary as unknown as (c: ExecutionContext) => string + const small = theme.spacing.small as unknown as (c: ExecutionContext) => string + + expect(primary(withTheme(appTheme))).toBe("#ff0000") + expect(small(withTheme(appTheme))).toBe("4px") + }) + + it("resolves the latest theme on each invocation (theme switching)", () => { + const { theme } = mkTheme() + + const darkTheme: AppTheme = { + colors: { primary: "#111111", secondary: "#222222" }, + spacing: { small: "2px", large: "8px" }, + } + + const primary = theme.colors.primary as unknown as (c: ExecutionContext) => string + + expect(primary(withTheme(appTheme))).toBe("#ff0000") + expect(primary(withTheme(darkTheme))).toBe("#111111") + }) + + it("returns undefined for a path that does not exist on the theme", () => { + const { theme } = mkTheme() + + const missing = (theme.colors as any).tertiary as (c: ExecutionContext) => unknown + + expect(missing(withTheme(appTheme))).toBeUndefined() + }) + }) + + describe("useTheme hook", () => { + it("returns the theme provided by styled-components ThemeProvider", () => { + const { useTheme } = mkTheme() + + const wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ) + + const { result } = renderHook(() => useTheme(), { wrapper }) + + expect(result.current).toEqual(appTheme) + expect(result.current.colors.primary).toBe("#ff0000") + expect(result.current.spacing.large).toBe("16px") + }) + + it("reflects the active provider theme value", () => { + const { useTheme } = mkTheme() + + const otherTheme: AppTheme = { + colors: { primary: "#abcdef", secondary: "#fedcba" }, + spacing: { small: "1px", large: "2px" }, + } + + const wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ) + + const { result } = renderHook(() => useTheme(), { wrapper }) + + expect(result.current).toEqual(otherTheme) + }) + }) +}) From 5c6565813b950f1575c6e14d7631903421356a18 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 09:47:55 +0000 Subject: [PATCH 2/2] test(styled-tools): drop mkTheme missing-path test that bypassed type safety The removed test reached a key absent from AppTheme via an `as any` cast, exercising a path mkTheme's TransformDeep type forbids at compile time. The underlying missing-property runtime behavior is owned by mkProxy and already covered directly in mkProxy.spec.ts, so the test added no meaningful coverage. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01DXXPnGepYUGie3we1X9PTR --- packages/styled-tools/src/lib/mkTheme.spec.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/styled-tools/src/lib/mkTheme.spec.tsx b/packages/styled-tools/src/lib/mkTheme.spec.tsx index b83c82a7b..c12885727 100644 --- a/packages/styled-tools/src/lib/mkTheme.spec.tsx +++ b/packages/styled-tools/src/lib/mkTheme.spec.tsx @@ -52,14 +52,6 @@ describe("mkTheme", () => { expect(primary(withTheme(appTheme))).toBe("#ff0000") expect(primary(withTheme(darkTheme))).toBe("#111111") }) - - it("returns undefined for a path that does not exist on the theme", () => { - const { theme } = mkTheme() - - const missing = (theme.colors as any).tertiary as (c: ExecutionContext) => unknown - - expect(missing(withTheme(appTheme))).toBeUndefined() - }) }) describe("useTheme hook", () => {