diff --git a/e2e/svelte-4/package-lock.json b/e2e/svelte-4/package-lock.json index d7a358f..f339bf1 100644 --- a/e2e/svelte-4/package-lock.json +++ b/e2e/svelte-4/package-lock.json @@ -22,7 +22,7 @@ } }, "../..": { - "version": "1.0.0", + "version": "0.0.0-semantically-released", "dev": true, "license": "MIT", "devDependencies": { @@ -31,7 +31,7 @@ "eslint": "^9.5.0", "eslint-config-standard": "^17.1.0", "eslint-plugin-svelte": "^2.40.0", - "svelte": "^4.2.18", + "svelte": "^5.0.0-next.171", "svelte-check": "^3.8.4", "vite": "^5.3.1", "vitest": "^1.6.0" diff --git a/e2e/svelte-4/src/lib/Button.svelte b/e2e/svelte-4/src/lib/Button.svelte index da09774..212304b 100644 --- a/e2e/svelte-4/src/lib/Button.svelte +++ b/e2e/svelte-4/src/lib/Button.svelte @@ -1,11 +1,23 @@ - diff --git a/e2e/svelte-4/src/lib/main.css b/e2e/svelte-4/src/lib/main.css index 9647bda..6a3979c 100644 --- a/e2e/svelte-4/src/lib/main.css +++ b/e2e/svelte-4/src/lib/main.css @@ -12,3 +12,15 @@ body { box-sizing: border-box; margin: 0; } + +h1 { + font-size: 1.75rem; +} +h2 { + font-size: 1.5rem; +} + +h1, +h2 { + margin-bottom: 0.75rem; +} diff --git a/e2e/svelte-4/src/routes/labels/+page.svelte b/e2e/svelte-4/src/routes/labels/+page.svelte new file mode 100644 index 0000000..1a07011 --- /dev/null +++ b/e2e/svelte-4/src/routes/labels/+page.svelte @@ -0,0 +1,32 @@ + + +

Labelled Cartesian

+ +

Short - explicit

+ +Make popcorn + +

Short - implicit

+ +Make popcorn + +

Long

+ +Make popcorn + +

Long with objects

+ +Make popcorn diff --git a/e2e/svelte-4/src/routes/labels/custom/+page.svelte b/e2e/svelte-4/src/routes/labels/custom/+page.svelte new file mode 100644 index 0000000..fc022a1 --- /dev/null +++ b/e2e/svelte-4/src/routes/labels/custom/+page.svelte @@ -0,0 +1,69 @@ + + +

Custom label, string value

+ + + Make popcorn +
+ Props +
{label}
+
+
+ +

Custom label, object value

+ + + Make popcorn +
+ Props +
{customLabel(innerProps)}
+
+
+ + diff --git a/e2e/svelte-4/src/routes/labels/dark/+page.svelte b/e2e/svelte-4/src/routes/labels/dark/+page.svelte new file mode 100644 index 0000000..372526d --- /dev/null +++ b/e2e/svelte-4/src/routes/labels/dark/+page.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/eslint.config.js b/eslint.config.js index 1c75742..e4b4b07 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,5 +5,11 @@ const compat = new FlatCompat() export default [ // standard compatibility ...compat.extends('eslint-config-standard'), - ...eslintPluginSvelte.configs['flat/recommended'] + ...eslintPluginSvelte.configs['flat/recommended'], + { + rules: { + 'no-multi-str': 0, + 'operator-linebreak': ['error', 'before'] + } + } ] diff --git a/jsconfig.json b/jsconfig.json index 5c316e9..0b38c96 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -9,6 +9,7 @@ "sourceMap": true, "strict": true, "module": "ESNext", + "lib": ["ESNext"], "moduleResolution": "Bundler" }, "exclude": ["e2e/**"] diff --git a/lib/Cartesian.svelte b/lib/Cartesian.svelte index 4c458a9..b4fd405 100644 --- a/lib/Cartesian.svelte +++ b/lib/Cartesian.svelte @@ -1,5 +1,5 @@ -
+ + +
{#each cartesianProps as innerProps} -
+ {@const label = labels && createLabel(innerProps, { verbosity: labels })} +
{#if asChild} - +
+ +
+ {#if labels} +
+ +
{label}
+
+
+ {/if} {:else} - - - +
+ + + +
+ {#if labels} +
+ +
{label}
+
+
+ {/if} {/if}
{/each}
diff --git a/lib/Cartesian.svelte.d.ts b/lib/Cartesian.svelte.d.ts index c938703..939307e 100644 --- a/lib/Cartesian.svelte.d.ts +++ b/lib/Cartesian.svelte.d.ts @@ -14,6 +14,17 @@ interface Props { * @default false */ asChild?: boolean + /** + * Generate labels under every iteration. + * + * - **true**: same as `'short'`. + * - **short**: display comma-separated values, skip objects. + * - **long**: display line-separated key-value pairs, represent object values + * as their type name. + * - **long-with-objects**: same as `'long'` but with full object definitions. + * @default undefined + */ + labels?: undefined | boolean | 'short' | 'long' | 'long-with-objects' /** * Disable built-in CSS. * @default false @@ -26,6 +37,10 @@ interface Props { divAttributes?: RestProps } +/** + * A single component that helps render prop combinations + * (the "Cartesian Product") for visual regression testing. + */ export default class Cartesian extends SvelteComponent< Props, {}, @@ -38,5 +53,15 @@ export default class Cartesian extends SvelteComponent< */ innerProps: Record } + label: { + /** + * The generated label. Hint: use `
` to render provided newline characters.
+       */
+      label: string
+      /**
+       * A single combination of props.
+       */
+      innerProps: Record
+    }
   }
 > {}
diff --git a/lib/cartesian.js b/lib/cartesian.js
index 3a10fdc..37eb8cc 100644
--- a/lib/cartesian.js
+++ b/lib/cartesian.js
@@ -1,9 +1,13 @@
 /**
-  * Convert props with arrays of values into their
-  * Cartesian Product: an array of prop combinations.
-  * @param {{[key: string]: any[]}} obj
-  * @returns {{ [key: string]: any }[]}
-  */
+ * @typedef {{ [key: string]: any }} CartesianProp
+ */
+
+/**
+ * Convert props with arrays of values into their
+ * Cartesian Product: an array of prop combinations.
+ * @param {{[key: string]: any[]}} obj
+ * @returns {CartesianProp[]}
+ */
 export function getCartesianProduct (obj) {
   const entries = Object.entries(obj)
 
@@ -31,3 +35,52 @@ export function getCartesianProduct (obj) {
 
   return result
 }
+
+/**
+ * Creates a label to render for a given component combination.
+ * @param {CartesianProp} innerProps
+ * @param {{verbosity?: boolean | 'short' | 'long' | 'long-with-objects'}} [options={ verbosity: 'short' }]
+ */
+export function createLabel (
+  innerProps,
+  { verbosity } = { verbosity: 'short' }
+) {
+  const label = []
+  const shortVerbosity = verbosity === 'short' || verbosity === true
+  const joinCharacter = shortVerbosity ? ', ' : '\n'
+
+  for (const [key, value] of Object.entries(innerProps)) {
+    if (
+      shortVerbosity
+      && typeof value !== 'string'
+      && typeof value !== 'number'
+      && typeof value !== 'boolean'
+    ) {
+      // Skip symbols and objects for 'short' labels
+      continue
+    }
+
+    let refinedValue = value
+
+    // Long verbosity treatment
+    if (
+      verbosity === 'long'
+      && typeof value !== 'string'
+      && typeof value !== 'number'
+    ) {
+      refinedValue = typeof value
+    } else if (verbosity === 'long-with-objects' && typeof value === 'object') {
+      refinedValue = JSON.stringify(value, null, 1)
+    }
+
+    if (verbosity === 'long' || verbosity === 'long-with-objects') {
+      label.push(`${key}: ${refinedValue}`)
+    } else if (shortVerbosity && typeof value === 'boolean') {
+      label.push(`${key}=${value}`)
+    } else {
+      label.push(refinedValue)
+    }
+  }
+
+  return label.join(joinCharacter)
+}
diff --git a/package.json b/package.json
index 46c4332..4cef3c4 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
   ],
   "scripts": {
     "check": "svelte-check",
-    "test:unit": "vitest"
+    "test": "vitest"
   },
   "author": "Enrico Sacchetti ",
   "license": "MIT",
diff --git a/tests/cartesian.test.js b/tests/cartesian.test.js
index 591dc1c..8a57a40 100644
--- a/tests/cartesian.test.js
+++ b/tests/cartesian.test.js
@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import { getCartesianProduct } from '../lib/cartesian'
+import { createLabel, getCartesianProduct } from '../lib/cartesian'
 
 describe('getCartesianProduct', () => {
   it('returns prop combinations', () => {
@@ -38,3 +38,64 @@ describe('getCartesianProduct', () => {
     ])
   })
 })
+
+describe('createLabel', () => {
+  it('returns short labels (default behaviour)', () => {
+    expect(createLabel({ variant: 'primary' }))
+      .toBe('primary')
+
+    expect(createLabel({ variant: 'primary' }, { verbosity: 'short' }))
+      .toBe('primary')
+
+    expect(createLabel({ variant: 'primary' }, { verbosity: true }))
+      .toBe('primary')
+
+    expect(createLabel({ variant: 'primary', size: 'md' }))
+      .toBe('primary, md')
+
+    expect(createLabel({ variant: 'primary', disabled: true }), 'handles booleans')
+      .toBe('primary, disabled=true')
+  })
+
+  it('returns long labels', () => {
+    expect(createLabel({ variant: 'primary' }, { verbosity: 'long' }))
+      .toBe('variant: primary')
+
+    expect(createLabel({
+      variant: 'primary',
+      size: 'md',
+      obj: { hello: 'world' }
+    }, { verbosity: 'long' }))
+      .toBe('variant: primary\nsize: md\nobj: object')
+  })
+
+  it('handles functions and symbols (short)', () => {
+    expect(createLabel({
+      variant: 'primary',
+      cb: (/** @type {Event} */ e) => {
+        e.preventDefault()
+      },
+      obj: { hello: 'world' },
+      sym: Symbol('foo')
+    }))
+      .toBe('primary')
+  })
+
+  it('returns object contents', () => {
+    expect(createLabel({
+      variant: 'primary',
+      cb: (/** @type {Event} */ e) => {
+        e.preventDefault()
+      },
+      obj: { hello: 'world' }
+    }, { verbosity: 'long-with-objects' }))
+      .toBe(
+        'variant: primary\n\
+cb: (/** @type {Event} */ e) => {\n\
+        e.preventDefault()\n\
+      }\n\
+obj: {\n\
+ "hello": "world"\n\
+}')
+  })
+})