@usequeek/theme-kit 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +1 -1
  3. package/README.md +164 -21
  4. package/api/collections.ts +14 -1
  5. package/api/orders.ts +15 -1
  6. package/apps/follow-card.tsx +5 -0
  7. package/apps/subscribe-form.tsx +2 -2
  8. package/components/auth/auth-flow.tsx +12 -0
  9. package/components/cart/cart-panel-controller.tsx +13 -0
  10. package/components/checkout/default-checkout-shell.tsx +5 -0
  11. package/components/checkout/policy-links.tsx +4 -0
  12. package/components/image.tsx +10 -0
  13. package/components/language-switcher.tsx +1 -0
  14. package/components/powered-by-queek.tsx +4 -3
  15. package/components/product-media-frame.tsx +5 -0
  16. package/components/product-metafields.tsx +9 -0
  17. package/components/variant-picker.css +4 -7
  18. package/edit-mode.tsx +4 -3
  19. package/hooks/use-address-autocomplete.ts +9 -0
  20. package/hooks/use-auth-flow.tsx +8 -13
  21. package/hooks/use-auth.ts +10 -0
  22. package/hooks/use-cart.ts +12 -0
  23. package/hooks/use-categories.ts +16 -2
  24. package/hooks/use-href.ts +9 -4
  25. package/hooks/use-posts.ts +16 -8
  26. package/hooks/use-product-detail.ts +25 -11
  27. package/hooks/use-product-questions.ts +16 -3
  28. package/hooks/use-products.ts +44 -22
  29. package/hooks/use-related-products.ts +17 -5
  30. package/hooks/use-resolve-coordinates.ts +9 -0
  31. package/hooks/use-reviews.ts +16 -2
  32. package/hooks/use-search.ts +10 -0
  33. package/hooks/use-shop-cart.ts +8 -0
  34. package/hooks/use-shop.ts +19 -6
  35. package/hooks/use-variant-selection.ts +23 -19
  36. package/index.ts +10 -11
  37. package/locales/en.default.json +4 -0
  38. package/navigation.tsx +2 -0
  39. package/package.json +29 -8
  40. package/page-renderer.tsx +11 -0
  41. package/provider.tsx +29 -3
  42. package/sdk/client.ts +183 -36
  43. package/sdk/error-handler.ts +18 -10
  44. package/sdk/queek-client.ts +12 -2
  45. package/sdk/server-store-client.ts +2 -2
  46. package/shared-blocks/embed.tsx +7 -3
  47. package/shared-blocks/manifest.ts +1 -1
  48. package/shared-blocks/product-qa.tsx +1 -1
  49. package/stores/address-modal-store.ts +1 -0
  50. package/stores/auth-modal-store.ts +1 -0
  51. package/stores/cart-panel-store.ts +1 -0
  52. package/stores/cart-store.ts +12 -7
  53. package/stores/checkout-store.ts +4 -4
  54. package/stores/location-store.ts +5 -0
  55. package/stores/orders-sheet-store.ts +1 -0
  56. package/stores/product-modal-store.ts +1 -0
  57. package/stores/search-modal-store.ts +1 -0
  58. package/stores/user-store.ts +1 -1
  59. package/strings/pseudo-locale.ts +1 -1
  60. package/theme-context.tsx +11 -1
  61. package/theme-mount.tsx +14 -0
  62. package/utils/format.ts +10 -1
  63. package/utils/menu-link.ts +8 -0
  64. package/utils/to-error.ts +4 -0
  65. package/utils/vendor-host.ts +1 -2
  66. package/scripts/verify-package.ts +0 -102
  67. package/tests/apps.test.tsx +0 -33
  68. package/tests/auth-email-errors.test.tsx +0 -55
  69. package/tests/currency-contract.test.ts +0 -85
  70. package/tests/default-metaobject-page.test.tsx +0 -56
  71. package/tests/display-format.test.ts +0 -403
  72. package/tests/fixtures/external-theme.tsx +0 -90
  73. package/tests/image-loading.test.tsx +0 -31
  74. package/tests/language-menu.test.tsx +0 -598
  75. package/tests/language-switcher.test.tsx +0 -350
  76. package/tests/list-pagination.test.tsx +0 -71
  77. package/tests/locale-switch.test.ts +0 -197
  78. package/tests/locale-transport.test.tsx +0 -321
  79. package/tests/manifest-parity.test.ts +0 -54
  80. package/tests/markdown-render.test.ts +0 -46
  81. package/tests/menu-placement.test.ts +0 -63
  82. package/tests/metaobjects-block.test.tsx +0 -70
  83. package/tests/navigation.test.tsx +0 -30
  84. package/tests/orders-walk.test.ts +0 -48
  85. package/tests/page-renderer-signature.test.tsx +0 -42
  86. package/tests/powered-by-queek.test.tsx +0 -65
  87. package/tests/powered-by-url.test.ts +0 -49
  88. package/tests/product-images.test.ts +0 -242
  89. package/tests/product-metafields.test.tsx +0 -154
  90. package/tests/public-surface.test.ts +0 -35
  91. package/tests/shop-query.test.ts +0 -64
  92. package/tests/shopper-chrome-strings.test.tsx +0 -271
  93. package/tests/storefront-client-scope.test.ts +0 -43
  94. package/tests/theme-hook-placement.test.tsx +0 -179
  95. package/tests/theme-strings-client.test.tsx +0 -310
  96. package/tests/theme-strings-pseudo.test.tsx +0 -171
  97. package/tests/theme-strings-typed.test.tsx +0 -97
  98. package/tests/theme-strings.test.tsx +0 -401
  99. package/tests/theme-typed-keys.fixture.ts +0 -76
  100. package/tests/use-shop-prefetch.test.tsx +0 -87
package/theme-context.tsx CHANGED
@@ -6,6 +6,10 @@ import type { ThemeModule } from './types/theme';
6
6
 
7
7
  const ThemeContext = createContext<ThemeModule | null>(null);
8
8
 
9
+ /**
10
+ * Makes a theme module available to `useTheme()`. `ThemeMount` renders it for
11
+ * you; use it directly only when you mount a theme yourself.
12
+ */
9
13
  export function ThemeProvider({
10
14
  theme,
11
15
  children,
@@ -18,11 +22,17 @@ export function ThemeProvider({
18
22
  );
19
23
  }
20
24
 
25
+ /**
26
+ * The mounted theme module (manifest, pages, header and footer variants).
27
+ *
28
+ * @throws {Error} when called outside `ThemeMount` or `ThemeProvider`. Use
29
+ * `useOptionalTheme()` where a theme may be absent.
30
+ */
21
31
  export function useTheme(): ThemeModule {
22
32
  const theme = useContext(ThemeContext);
23
33
 
24
34
  if (!theme) {
25
- throw new Error('useTheme must be used within ThemeProvider');
35
+ throw new Error('useTheme() was called outside <ThemeProvider>. Render the component inside <ThemeMount> (or <ThemeProvider theme={...}>).');
26
36
  }
27
37
 
28
38
  return theme;
package/theme-mount.tsx CHANGED
@@ -49,6 +49,7 @@ class ThemeBoundary extends Component<
49
49
  }
50
50
  }
51
51
 
52
+ /** Props for {@link ThemeMount}. */
52
53
  export interface ThemeMountProps {
53
54
  theme: ThemeModule;
54
55
  headerProps: HeaderProps;
@@ -59,6 +60,19 @@ export interface ThemeMountProps {
59
60
  children: ReactNode;
60
61
  }
61
62
 
63
+ /**
64
+ * Render a theme: its layout, the header and footer chosen by variant id, and
65
+ * `children` (the page) between them. A header or footer that throws is
66
+ * replaced by a visible error box naming the part, so one broken component
67
+ * does not blank the screen.
68
+ *
69
+ * @example
70
+ * <StorefrontProvider vendor={vendor} config={config} menus={menus}>
71
+ * <ThemeMount theme={theme} headerProps={headerProps} footerProps={footerProps}>
72
+ * <HomePage blocks={blocks} />
73
+ * </ThemeMount>
74
+ * </StorefrontProvider>
75
+ */
62
76
  export function ThemeMount({
63
77
  theme,
64
78
  headerProps,
package/utils/format.ts CHANGED
@@ -125,7 +125,7 @@ export function formatRelativeTime(
125
125
  /**
126
126
  * Platform default — the LAST resort only. Item rows carry their own
127
127
  * `currency` code (see normalizeProduct) and store-wide display uses the
128
- * vendor's currency (cart-store, set from vendor-shell on mount). A missing
128
+ * vendor's currency (held in the cart store, which the storefront host sets on mount). A missing
129
129
  * or malformed code must resolve through this function, never a bare
130
130
  * `?? 'NGN'` at a call site, so the fallback stays in exactly one place.
131
131
  */
@@ -144,6 +144,14 @@ export function normalizeCurrencyCode(value: unknown, fallback: string = PLATFOR
144
144
  return /^[A-Z]{3}$/.test(cleanFallback) ? cleanFallback : PLATFORM_DEFAULT_CURRENCY;
145
145
  }
146
146
 
147
+ /**
148
+ * Format an amount in a currency (ISO 4217 code), in the shopper's locale.
149
+ * Decimals show only when the amount has them (`₦5,000`, `₦0.28`). An invalid
150
+ * currency code falls back to the platform default. Never throws.
151
+ *
152
+ * @example
153
+ * formatMoney(5000, 'NGN'); // "₦5,000"
154
+ */
147
155
  export function formatMoney(
148
156
  amount: number,
149
157
  currency = PLATFORM_DEFAULT_CURRENCY,
@@ -163,6 +171,7 @@ export function formatMoney(
163
171
  }
164
172
  }
165
173
 
174
+ /** A whole-number percentage string, e.g. `toPercent(12.4)` is `"12%"`. */
166
175
  export function toPercent(value: number): string {
167
176
  return `${Math.max(0, Math.round(value))}%`;
168
177
  }
@@ -11,6 +11,14 @@ export function menuItemKey(item: MenuItem, index: number): string {
11
11
  return `${item.type}:${item.ref ?? ''}:${item.meta?.anchor ?? ''}:${index}`;
12
12
  }
13
13
 
14
+ /**
15
+ * The link target for a menu item: pages, collections, the shop, the blog, posts
16
+ * and galleries resolve under `basePath`; a `url` item is used as is; anything
17
+ * unknown becomes `#`. Pass `useStorefront().basePath` (or use `useHref`).
18
+ *
19
+ * @example
20
+ * <Link href={menuItemToHref(item, basePath)}>{item.label}</Link>
21
+ */
14
22
  export function menuItemToHref(item: MenuItem, basePath: string = ''): string {
15
23
  switch (item.type) {
16
24
  case 'page': {
@@ -0,0 +1,4 @@
1
+ /** Normalises anything a `catch` can hand back into an `Error`, so hooks can expose one `error` type. */
2
+ export function toError(value: unknown): Error {
3
+ return value instanceof Error ? value : new Error(String(value));
4
+ }
@@ -84,8 +84,7 @@ export async function resolveCustomDomainVendorSlugRemote(hostname: string): Pro
84
84
  }
85
85
 
86
86
  // Middleware runs server-side — prefer the server-only absolute URL. The
87
- // public var can be a same-origin relative path (checkout-proxy rollout),
88
- // which `fetch()` can't resolve without a document origin here.
87
+ // public var can be a same-origin relative path, which `fetch()` can't resolve without a document origin here.
89
88
  const baseUrl = process.env.QUEEK_API_URL ?? process.env.NEXT_PUBLIC_QUEEK_API_URL;
90
89
  if (!baseUrl) {
91
90
  return null;
@@ -1,102 +0,0 @@
1
- /**
2
- * Consumes the kit the way an OUTSIDER does, from a packed tarball.
3
- *
4
- * WHY THIS EXISTS. @usequeek/theme-kit@0.1.0 was published and was broken for
5
- * every external consumer:
6
- *
7
- * node_modules/@usequeek/theme-kit/provider.tsx(58,5):
8
- * error TS2503: Cannot find namespace 'JSX'.
9
- *
10
- * 42 modules annotated returns as `JSX.Element`, relying on a GLOBAL JSX
11
- * namespace that came from a host app's `next-env.d.ts` — not from React.
12
- * Inside a host app the kit free-rode on that file, so tsc, the tests, the
13
- * build and the render smoke were ALL green. The workspace supplies exactly the
14
- * file whose absence is the bug, which is why no in-repo check could ever have
15
- * found it. (Every module now does `import type { JSX } from 'react'`, and this
16
- * script proves it.)
17
- *
18
- * It packs the CURRENT SOURCE rather than installing the published version, so
19
- * it fails before a broken version ships instead of after. Installing from the
20
- * registry would only ever test the last release.
21
- */
22
- import { execFileSync } from 'node:child_process';
23
- import { mkdtempSync, rmSync, writeFileSync, readFileSync, mkdirSync, readdirSync } from 'node:fs';
24
- import { tmpdir } from 'node:os';
25
- import { dirname, join, resolve } from 'node:path';
26
- import { fileURLToPath } from 'node:url';
27
-
28
- // The kit package is `"type": "module"`, so `__dirname` does not exist here.
29
- const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
30
- const KIT = ROOT;
31
-
32
- function run(cmd: string, args: string[], cwd: string): string {
33
- return execFileSync(cmd, args, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
34
- }
35
-
36
- function main(): void {
37
- const sandbox = mkdtempSync(join(tmpdir(), 'kit-consumer-'));
38
- let failed = false;
39
-
40
- try {
41
- const kitPkg = JSON.parse(readFileSync(join(KIT, 'package.json'), 'utf-8')) as {
42
- name: string; version: string; peerDependencies?: Record<string, string>;
43
- };
44
-
45
- console.log(`verify-package: packing ${kitPkg.name}@${kitPkg.version} and consuming it as an outsider`);
46
- run('npm', ['pack', '--pack-destination', sandbox], KIT);
47
- const tarball = readdirSync(sandbox).find((f) => f.endsWith('.tgz'));
48
- if (!tarball) throw new Error('npm pack produced no tarball');
49
-
50
- // Peers are declared by the kit; pin the ones the fixture needs to compile.
51
- const peers = kitPkg.peerDependencies ?? {};
52
- writeFileSync(join(sandbox, 'package.json'), JSON.stringify({
53
- name: 'kit-consumer', private: true, version: '1.0.0',
54
- dependencies: {
55
- [kitPkg.name]: `file:./${tarball}`,
56
- '@queekai/client-sdk': peers['@queekai/client-sdk'] ?? '*',
57
- next: '^16.0.0', react: '^19.0.0', 'react-dom': '^19.0.0',
58
- },
59
- devDependencies: { typescript: '^5', '@types/react': '^19', '@types/react-dom': '^19', '@types/node': '^22' },
60
- }, null, 2));
61
-
62
- // `bundler` resolution + no ambient Next types — exactly what a theme
63
- // developer's own project looks like. NOT this repo's tsconfig.
64
- writeFileSync(join(sandbox, 'tsconfig.json'), JSON.stringify({
65
- compilerOptions: {
66
- target: 'ES2017', lib: ['dom', 'dom.iterable', 'esnext'], jsx: 'react-jsx',
67
- module: 'esnext', moduleResolution: 'bundler', strict: true, noEmit: true,
68
- skipLibCheck: true, esModuleInterop: true, isolatedModules: true,
69
- },
70
- include: ['app/**/*.ts', 'app/**/*.tsx'],
71
- }, null, 2));
72
-
73
- mkdirSync(join(sandbox, 'app'), { recursive: true });
74
- writeFileSync(join(sandbox, 'app/theme.tsx'), readFileSync(join(ROOT, 'tests/fixtures/external-theme.tsx'), 'utf-8'));
75
-
76
- console.log('verify-package: installing from the tarball…');
77
- run('npm', ['install', '--no-audit', '--no-fund'], sandbox);
78
-
79
- console.log('verify-package: typechecking a real theme against it…');
80
- try {
81
- run(join(sandbox, 'node_modules/.bin/tsc'), ['--noEmit'], sandbox);
82
- console.log(`\n✓ ${kitPkg.name} is consumable from outside this repo.`);
83
- } catch (error) {
84
- const out = (error as { stdout?: string; stderr?: string });
85
- console.error('\n✗ the packed kit does NOT work for an external consumer:\n');
86
- console.error((out.stdout ?? '') + (out.stderr ?? ''));
87
- console.error('Errors inside node_modules/@usequeek/theme-kit are PACKAGE bugs — the kit is');
88
- console.error('relying on something only this repo provides. Errors in app/theme.tsx mean the');
89
- console.error('fixture drifted from the real API.');
90
- failed = true;
91
- }
92
- } catch (error) {
93
- console.error('✗ verify-package failed to run:', (error as Error).message);
94
- failed = true;
95
- } finally {
96
- rmSync(sandbox, { recursive: true, force: true });
97
- }
98
-
99
- process.exit(failed ? 1 : 0);
100
- }
101
-
102
- main();
@@ -1,33 +0,0 @@
1
- import React from 'react';
2
- import { renderToStaticMarkup } from 'react-dom/server';
3
- import { describe, expect, it } from 'vitest';
4
- import * as apps from '../apps';
5
- import { AppBlocks, AppBlocksProvider } from '../apps';
6
-
7
- describe('apps — the theme-facing app-block slot', () => {
8
- it('is exactly the provider, the slot, and the shared types', () => {
9
- expect(Object.keys(apps).sort()).toEqual(['AppBlocks', 'AppBlocksProvider']);
10
- });
11
-
12
- it('renders nothing without a host registration (SSR-safe)', () => {
13
- const html = renderToStaticMarkup(
14
- React.createElement(AppBlocks, { target: 'product', productId: 'p1' }),
15
- );
16
-
17
- expect(html).toBe('');
18
- });
19
-
20
- it('renders whatever the host registered, with the slot props', () => {
21
- function HostBlocks({ target, productId }: { target: 'product'; productId?: string }) {
22
- return <div data-host-blocks={`${target}:${productId ?? ''}`} />;
23
- }
24
-
25
- const html = renderToStaticMarkup(
26
- <AppBlocksProvider renderBlocks={HostBlocks}>
27
- <AppBlocks target="product" productId="p1" vendorSlug="glow" />
28
- </AppBlocksProvider>,
29
- );
30
-
31
- expect(html).toContain('data-host-blocks="product:p1"');
32
- });
33
- });
@@ -1,55 +0,0 @@
1
- /**
2
- * Auth error English pins.
3
- *
4
- * `mapEmailError` maps SDK failure codes to `t('auth.error.*')` strings, and
5
- * three more error keys are set directly in the provider. Every English value
6
- * below is resolved through the REAL default dictionary, so renaming a key or
7
- * changing any English value fails here.
8
- */
9
- import { describe, expect, it } from 'vitest';
10
- import { QueekSdkError } from '@queekai/client-sdk';
11
- import { t, defaultThemeStrings } from '../strings/theme-strings';
12
- import { mapEmailError } from '../hooks/use-auth-flow';
13
-
14
- const tt = (key: string): string => t(defaultThemeStrings, key);
15
-
16
- function sdkError(code: string, message: string): QueekSdkError {
17
- return new QueekSdkError(message, code, 400);
18
- }
19
-
20
- describe('mapEmailError code -> English pins', () => {
21
- it.each([
22
- ['invalid_credentials', 'Wrong email or password. Please try again.'],
23
- ['login_mode_mismatch', 'This account uses a different sign-in method (Google or phone).'],
24
- ['account_already_exists', 'Email already registered. Please sign in instead.'],
25
- ['phone_already_exists', 'Phone number already in use.'],
26
- ['otp_resend_too_soon', 'Please wait before requesting another code.'],
27
- ['invalid_otp', 'Incorrect code. Please try again.'],
28
- ] as Array<[string, string]>)('code %s maps to its exact English', (code, english) => {
29
- expect(mapEmailError(sdkError(code, 'server message'), tt)).toBe(english);
30
- });
31
-
32
- it('passes an unknown-code server message through untouched', () => {
33
- expect(mapEmailError(sdkError('something_else', 'Server says no.'), tt)).toBe('Server says no.');
34
- });
35
-
36
- it('falls back to generic English when the server message is empty', () => {
37
- expect(mapEmailError(sdkError('something_else', ''), tt)).toBe('Something went wrong. Please try again.');
38
- });
39
-
40
- it('falls back to generic English for non-SDK failures', () => {
41
- expect(mapEmailError(new Error('boom'), tt)).toBe('Something went wrong. Please try again.');
42
- expect(mapEmailError('boom', tt)).toBe('Something went wrong. Please try again.');
43
- });
44
- });
45
-
46
- describe('direct-set auth.error.* English pins', () => {
47
- it.each([
48
- ['auth.error.firstname', 'Please enter your first name.'],
49
- ['auth.error.register', 'Registration failed. Try again.'],
50
- ['auth.error.google', 'Could not connect to Google sign-in. Please try again.'],
51
- ['auth.error.generic', 'Something went wrong. Please try again.'],
52
- ] as Array<[string, string]>)('%s resolves to its exact English', (key, english) => {
53
- expect(tt(key)).toBe(english);
54
- });
55
- });
@@ -1,85 +0,0 @@
1
- import { readFileSync, readdirSync, statSync } from 'node:fs';
2
- import { join, relative, resolve } from 'node:path';
3
- import { describe, expect, it } from 'vitest';
4
- import { formatMoney, normalizeCurrencyCode, PLATFORM_DEFAULT_CURRENCY } from '../utils/format';
5
- import { extractProducts, normalizeProduct } from '../utils/product-normalizer';
6
- import { useCartStore } from '../stores/cart-store';
7
-
8
- /**
9
- * Client readers do not use `meta.currency`. List-level `meta.currency` stamps the PLATFORM default (NGN) even on
10
- * non-NGN stores, so the item's own `currency` code is the source and the
11
- * vendor's currency is the store-wide source. Missing codes fall back to the
12
- * platform default as a LAST resort, in exactly one place.
13
- */
14
- describe('currency resolution', () => {
15
- it('exposes NGN as the platform default last resort', () => {
16
- expect(PLATFORM_DEFAULT_CURRENCY).toBe('NGN');
17
- });
18
-
19
- it('normalizeCurrencyCode keeps valid item/vendor codes', () => {
20
- expect(normalizeCurrencyCode('USD')).toBe('USD');
21
- expect(normalizeCurrencyCode('ngn')).toBe('NGN');
22
- expect(normalizeCurrencyCode(' eur ')).toBe('EUR');
23
- });
24
-
25
- it('normalizeCurrencyCode falls back instead of passing garbage through', () => {
26
- expect(normalizeCurrencyCode(undefined)).toBe('NGN');
27
- expect(normalizeCurrencyCode(null)).toBe('NGN');
28
- expect(normalizeCurrencyCode('')).toBe('NGN');
29
- expect(normalizeCurrencyCode('US')).toBe('NGN');
30
- expect(normalizeCurrencyCode('USDD')).toBe('NGN');
31
- expect(normalizeCurrencyCode(42)).toBe('NGN');
32
- });
33
-
34
- it('normalizeCurrencyCode honors an explicit vendor fallback', () => {
35
- expect(normalizeCurrencyCode(undefined, 'USD')).toBe('USD');
36
- expect(normalizeCurrencyCode('', 'eur')).toBe('EUR');
37
- expect(normalizeCurrencyCode(undefined, 'bogus')).toBe('NGN');
38
- });
39
-
40
- it('normalizeProduct keeps the item currency, ignoring list-level meta', () => {
41
- const envelope = {
42
- data: [{ id: 'p1', title: 'Tote', price: 5000, currency: 'usd' }],
43
- meta: { currency: { code: 'NGN', symbol: '₦' } },
44
- };
45
- const [raw] = extractProducts(envelope);
46
- expect(normalizeProduct(raw).currency).toBe('USD');
47
- });
48
-
49
- it('normalizeProduct falls back to the platform default when the item has none', () => {
50
- const [raw] = extractProducts({ data: [{ id: 'p2', title: 'Cap', price: 1000 }] });
51
- expect(normalizeProduct(raw).currency).toBe('NGN');
52
- });
53
-
54
- it('formatMoney formats through the item code, not a wired symbol', () => {
55
- expect(formatMoney(12500)).toBe('₦12,500');
56
- const usd = formatMoney(5000, 'USD');
57
- expect(usd).toContain('5,000');
58
- expect(usd).not.toContain('₦');
59
- });
60
-
61
- it('cart-store currency starts at the platform default and normalizes vendor init', () => {
62
- expect(useCartStore.getState().currency).toBe('NGN');
63
- useCartStore.getState().setCurrency('usd');
64
- expect(useCartStore.getState().currency).toBe('USD');
65
- useCartStore.getState().setCurrency('');
66
- expect(useCartStore.getState().currency).toBe('NGN');
67
- });
68
-
69
- it('no kit source reads list-level meta.currency', () => {
70
- const ROOT = resolve(__dirname, '..');
71
- const walk = (dir: string): string[] => readdirSync(dir).flatMap((entry) => {
72
- const path = join(dir, entry);
73
- if (entry === 'node_modules' || entry === 'tests' || entry.startsWith('.')) return [];
74
- if (statSync(path).isDirectory()) return walk(path);
75
- return /\.(ts|tsx)$/.test(entry) ? [path] : [];
76
- });
77
- const offenders = walk(ROOT).flatMap((file) => {
78
- const source = readFileSync(file, 'utf8');
79
- // Real reads use `?.` or a `.meta` access (`payload?.meta?.currency`,
80
- // `data.meta.currency`); prose comments naming the contract don't count.
81
- return /meta\?\.\s*currency|\.meta\.currency/.test(source) ? [relative(ROOT, file)] : [];
82
- });
83
- expect(offenders).toEqual([]);
84
- });
85
- });
@@ -1,56 +0,0 @@
1
- import React from 'react';
2
- import { renderToStaticMarkup } from 'react-dom/server';
3
- import { describe, expect, it } from 'vitest';
4
- import { DefaultMetaobjectPage } from '../components/default-metaobject-page';
5
- import type { MetaobjectEntry } from '../types/product';
6
- import type { MetaobjectPageDefinition } from '../types/theme';
7
-
8
- function render(fields: MetaobjectEntry['fields'], fieldDefs: MetaobjectPageDefinition['fields']): string {
9
- const entry: MetaobjectEntry = {
10
- p_id: 1,
11
- type: 'designer',
12
- handle: 'adaeze',
13
- display_name: 'Adaeze',
14
- fields,
15
- };
16
- const definition: MetaobjectPageDefinition = { type: 'designer', name: 'Designer', fields: fieldDefs };
17
-
18
- return renderToStaticMarkup(React.createElement(DefaultMetaobjectPage, { entry, definition }));
19
- }
20
-
21
- describe('DefaultMetaobjectPage', () => {
22
- it('renders the entry display name as the title', () => {
23
- const html = render({}, []);
24
- expect(html).toContain('core-metaobject-page__title');
25
- expect(html).toContain('Adaeze');
26
- });
27
-
28
- it('renders a text field with its definition label', () => {
29
- const html = render({ bio: 'Lagos-based designer' }, [{ key: 'bio', name: 'Biography', type: 'single_line_text' }]);
30
- expect(html).toContain('Biography');
31
- expect(html).toContain('Lagos-based designer');
32
- });
33
-
34
- it('falls back to a humanised key label when no definition field matches', () => {
35
- const html = render({ years_active: '10' }, []);
36
- expect(html).toContain('Years active');
37
- });
38
-
39
- it('renders a boolean field as Yes/No', () => {
40
- const html = render({ verified: true }, [{ key: 'verified', name: 'Verified', type: 'boolean' }]);
41
- expect(html).toContain('>Yes<');
42
- });
43
-
44
- it('renders a url field as a link', () => {
45
- const html = render({ portfolio: 'https://example.com/adaeze' }, [{ key: 'portfolio', name: 'Portfolio', type: 'url' }]);
46
- expect(html).toContain('<a');
47
- expect(html).toContain('href="https://example.com/adaeze"');
48
- });
49
-
50
- it('renders a list field as bullets', () => {
51
- const html = render({ tags: ['couture', 'bridal'] }, [{ key: 'tags', name: 'Tags', type: 'list.single_line_text' }]);
52
- expect(html).toContain('<ul');
53
- expect(html).toContain('couture');
54
- expect(html).toContain('bridal');
55
- });
56
- });