@akinon/pz-theme 2.0.30-beta.0 → 2.0.30

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 (46) hide show
  1. package/CHANGELOG.md +6 -3
  2. package/jest/__mocks__/akinon-next-hooks.js +15 -0
  3. package/jest/__mocks__/akinon-next-image.js +22 -0
  4. package/jest/__mocks__/akinon-next-loader-spinner.js +7 -0
  5. package/jest/__mocks__/akinon-next-localization.js +10 -0
  6. package/jest/__mocks__/akinon-next-modal.js +11 -0
  7. package/jest/__mocks__/akinon-next-price.js +9 -0
  8. package/jest/__mocks__/akinon-next-rtk.js +30 -0
  9. package/jest/__mocks__/akinon-next-utils.js +8 -0
  10. package/jest/__mocks__/next-link.js +11 -0
  11. package/jest/__mocks__/settings.js +13 -0
  12. package/jest/setup.ts +33 -0
  13. package/jest.config.js +24 -3
  14. package/package.json +6 -3
  15. package/src/__tests__/theme-block.test.tsx +46 -0
  16. package/src/blocks/__tests__/accordion-block.test.tsx +178 -0
  17. package/src/blocks/__tests__/button-block.test.tsx +99 -0
  18. package/src/blocks/__tests__/counter-block.test.tsx +57 -0
  19. package/src/blocks/__tests__/embed-block.test.tsx +89 -0
  20. package/src/blocks/__tests__/group-block.test.tsx +175 -0
  21. package/src/blocks/__tests__/hotspot-block.test.tsx +93 -0
  22. package/src/blocks/__tests__/icon-block.test.tsx +88 -0
  23. package/src/blocks/__tests__/image-block.test.tsx +139 -0
  24. package/src/blocks/__tests__/image-gallery-block.test.tsx +117 -0
  25. package/src/blocks/__tests__/link-block.test.tsx +184 -0
  26. package/src/blocks/__tests__/map-block.test.tsx +165 -0
  27. package/src/blocks/__tests__/slider-block.test.tsx +148 -0
  28. package/src/blocks/__tests__/text-block.test.tsx +87 -0
  29. package/src/blocks/__tests__/trivial-blocks.test.tsx +59 -0
  30. package/src/blocks/__tests__/video-block.test.tsx +196 -0
  31. package/src/blocks/counter-block.tsx +1 -1
  32. package/src/blocks/group-block.tsx +1 -1
  33. package/src/blocks/icon-block.tsx +11 -6
  34. package/src/blocks/video-block.tsx +1 -1
  35. package/src/sections/__tests__/before-after-section.test.tsx +139 -0
  36. package/src/sections/__tests__/countdown-campaign-banner-section.test.tsx +188 -0
  37. package/src/sections/__tests__/coupon-banner-section.test.tsx +188 -0
  38. package/src/sections/__tests__/divider-section.test.tsx +161 -0
  39. package/src/sections/__tests__/hover-showcase-section.test.tsx +193 -0
  40. package/src/sections/__tests__/image-hotspot-section.test.tsx +110 -0
  41. package/src/sections/__tests__/notification-banner-section.test.tsx +144 -0
  42. package/src/sections/__tests__/section-wrapper.test.tsx +164 -0
  43. package/src/sections/__tests__/stats-counter-section.test.tsx +245 -0
  44. package/src/sections/__tests__/tabs-section.test.tsx +115 -0
  45. package/src/sections/coupon-banner-section.tsx +1 -4
  46. package/src/theme-block.tsx +3 -1
package/CHANGELOG.md CHANGED
@@ -1,11 +1,14 @@
1
1
  # @akinon/pz-theme
2
2
 
3
- ## 2.0.30-beta.0
3
+ ## 2.0.30
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - Updated dependencies [cbbbfd75]
8
- - @akinon/next@2.0.30-beta.0
7
+ - ad612b5: ZERO-4588: Stop leaking the "Unknown block type" debug text to production for unregistered block types; the hint is now shown only in the designer (production renders nothing).
8
+ - ad612b5: ZERO-4588: Fix icon-block ignoring the active breakpoint (it was hardcoded to 'desktop'); mobile value/size/color overrides now apply on the mobile breakpoint.
9
+ - ad612b5: ZERO-4588: Fix counter-block dropping hidden child blocks in the designer (missing isDesigner guard); hidden children are now selectable in the editor, production rendering unchanged.
10
+ - af4cd11: ZERO-4588: Fix three render bugs found while adding component tests — video-block now renders the invalid-URL placeholder instead of crashing on a non-string value; group-block resolves its `tag` using the active breakpoint (was hardcoded to desktop); coupon-banner no longer collapses a numeric `12` border-radius (e.g. 12px/12rem) to 0px.
11
+ - @akinon/next@2.0.30
9
12
 
10
13
  ## 2.0.29
11
14
 
@@ -0,0 +1,15 @@
1
+ // Mock for @akinon/next/hooks (only useLocalization is consumed by pz-theme).
2
+ const useLocalization = () => ({
3
+ t: (path) => path,
4
+ setLocale: () => {},
5
+ setCurrency: () => {},
6
+ locale: 'en-us',
7
+ currency: 'USD',
8
+ locales: [{ value: 'en-us' }, { value: 'tr-tr' }],
9
+ currencies: ['USD'],
10
+ defaultLocaleValue: 'en-us',
11
+ defaultCurrencyCode: 'USD',
12
+ localeUrlStrategy: 'hide-default-locale'
13
+ });
14
+
15
+ module.exports = { __esModule: true, useLocalization };
@@ -0,0 +1,22 @@
1
+ // Mock for @akinon/next/components/image — a dumb <img> that drops next/image-only props.
2
+ const React = require('react');
3
+
4
+ const Image = (props) => {
5
+ const {
6
+ src,
7
+ alt = '',
8
+ fill,
9
+ priority,
10
+ loader,
11
+ quality,
12
+ placeholder,
13
+ blurDataURL,
14
+ unoptimized,
15
+ loading,
16
+ fetchPriority,
17
+ ...domProps
18
+ } = props || {};
19
+ return React.createElement('img', { src, alt, ...domProps });
20
+ };
21
+
22
+ module.exports = { __esModule: true, Image, default: Image };
@@ -0,0 +1,7 @@
1
+ // Mock for @akinon/next/components/loader-spinner.
2
+ const React = require('react');
3
+
4
+ const LoaderSpinner = () =>
5
+ React.createElement('span', { 'data-testid': 'loader-spinner' });
6
+
7
+ module.exports = { __esModule: true, LoaderSpinner, default: LoaderSpinner };
@@ -0,0 +1,10 @@
1
+ // Mock for @akinon/next/localization — values mirror the real enum.
2
+ module.exports = {
3
+ __esModule: true,
4
+ LocaleUrlStrategy: {
5
+ HideAllLocales: 'hide-all-locales',
6
+ HideDefaultLocale: 'hide-default-locale',
7
+ ShowAllLocales: 'show-all-locales',
8
+ Subdomain: 'subdomain'
9
+ }
10
+ };
@@ -0,0 +1,11 @@
1
+ // Mock for @akinon/next/components/modal — renders children when open, else nothing.
2
+ const React = require('react');
3
+
4
+ const Modal = (props) => {
5
+ const { open, isOpen, show, children } = props || {};
6
+ return open || isOpen || show
7
+ ? React.createElement(React.Fragment, null, children)
8
+ : null;
9
+ };
10
+
11
+ module.exports = { __esModule: true, Modal, default: Modal };
@@ -0,0 +1,9 @@
1
+ // Mock for @akinon/next/components/price — renders a simple span.
2
+ const React = require('react');
3
+
4
+ const Price = (props) => {
5
+ const { value, children } = props || {};
6
+ return React.createElement('span', { 'data-testid': 'price' }, children ?? value ?? '');
7
+ };
8
+
9
+ module.exports = { __esModule: true, Price, default: Price };
@@ -0,0 +1,30 @@
1
+ // Catch-all mock for @akinon/next/data/client/* RTK Query hooks.
2
+ // *Query hooks -> a result object; *Mutation hooks -> [trigger, state] tuple.
3
+ const queryResult = () => ({
4
+ data: undefined,
5
+ isLoading: false,
6
+ isFetching: false,
7
+ isSuccess: true,
8
+ isError: false,
9
+ error: undefined,
10
+ refetch: jest.fn()
11
+ });
12
+
13
+ const mutationTuple = () => [
14
+ jest.fn(() => ({ unwrap: () => Promise.resolve({}) })),
15
+ { isLoading: false, isSuccess: false, isError: false, reset: jest.fn() }
16
+ ];
17
+
18
+ const handler = {
19
+ get(_target, prop) {
20
+ if (prop === '__esModule') return true;
21
+ if (typeof prop !== 'string') return undefined;
22
+ if (prop === 'default') return proxy;
23
+ if (prop.endsWith('Mutation')) return () => mutationTuple();
24
+ if (prop.endsWith('Query')) return () => queryResult();
25
+ return () => undefined;
26
+ }
27
+ };
28
+
29
+ const proxy = new Proxy({}, handler);
30
+ module.exports = proxy;
@@ -0,0 +1,8 @@
1
+ // Mock for @akinon/next/utils (only these three are consumed by pz-theme).
2
+ // urlLocaleMatcherRegex MUST be a real RegExp (link-block calls String#replace with it).
3
+ module.exports = {
4
+ __esModule: true,
5
+ buildClientRequestUrl: (path) => path,
6
+ urlLocaleMatcherRegex: /^\/(?:[a-z]{2}-[a-z]{2})(?=\/|$)/i,
7
+ urlSchemes: ['http', 'tel:', 'mailto:', 'sms:', 'whatsapp:', 'skype:', 'ftp:', 'file:']
8
+ };
@@ -0,0 +1,11 @@
1
+ // Mock for next/link — a plain <a> (no app-router context needed in tests).
2
+ const React = require('react');
3
+
4
+ const Link = (props) => {
5
+ const { href, children, ...rest } = props || {};
6
+ const resolved =
7
+ typeof href === 'string' ? href : (href && href.pathname) || '#';
8
+ return React.createElement('a', { href: resolved, ...rest }, children);
9
+ };
10
+
11
+ module.exports = { __esModule: true, default: Link };
@@ -0,0 +1,13 @@
1
+ // Safety-net stub for the `settings` module (real one lives in the consuming app).
2
+ const settings = {
3
+ localization: {
4
+ locales: [{ value: 'en-us' }, { value: 'tr-tr' }],
5
+ defaultLocaleValue: 'en-us',
6
+ localeUrlStrategy: 'hide-default-locale',
7
+ currencies: ['USD'],
8
+ defaultCurrencyCode: 'USD'
9
+ }
10
+ };
11
+
12
+ module.exports = settings;
13
+ module.exports.default = settings;
package/jest/setup.ts ADDED
@@ -0,0 +1,33 @@
1
+ import '@testing-library/jest-dom';
2
+
3
+ // jsdom does not implement these; provide no-op shims so components that touch
4
+ // them on mount don't crash under test.
5
+ if (!window.matchMedia) {
6
+ window.matchMedia = ((query: string) => ({
7
+ matches: false,
8
+ media: query,
9
+ onchange: null,
10
+ addListener: () => {},
11
+ removeListener: () => {},
12
+ addEventListener: () => {},
13
+ removeEventListener: () => {},
14
+ dispatchEvent: () => false
15
+ })) as unknown as typeof window.matchMedia;
16
+ }
17
+
18
+ window.scrollTo = (() => {}) as typeof window.scrollTo;
19
+
20
+ class NoopObserver {
21
+ observe() {}
22
+ unobserve() {}
23
+ disconnect() {}
24
+ takeRecords() {
25
+ return [];
26
+ }
27
+ }
28
+
29
+ (window as unknown as Record<string, unknown>).IntersectionObserver =
30
+ (window as unknown as Record<string, unknown>).IntersectionObserver ||
31
+ NoopObserver;
32
+ (window as unknown as Record<string, unknown>).ResizeObserver =
33
+ (window as unknown as Record<string, unknown>).ResizeObserver || NoopObserver;
package/jest.config.js CHANGED
@@ -6,10 +6,31 @@ module.exports = {
6
6
  testEnvironment: 'jsdom',
7
7
  roots: ['<rootDir>/src'],
8
8
  testMatch: ['**/*.test.ts', '**/*.test.tsx'],
9
- // Stub side-effect CSS imports (e.g. react-multi-carousel styles) so that
10
- // importing a component never fails on a missing CSS transform.
9
+ setupFilesAfterEnv: ['<rootDir>/jest/setup.ts'],
10
+ // Component tests render block/section renderers which import @akinon/next
11
+ // subpaths and `settings`. We map each consumed subpath to a tiny mock so
12
+ // the real (app-coupled / RTK) modules are never loaded. Order matters:
13
+ // specific patterns must precede the data/client catch-all.
11
14
  moduleNameMapper: {
12
- '\\.(css|less|scss|sass)$': '<rootDir>/jest/style-mock.js'
15
+ '\\.(css|less|scss|sass)$': '<rootDir>/jest/style-mock.js',
16
+ '^settings$': '<rootDir>/jest/__mocks__/settings.js',
17
+ '^next/link$': '<rootDir>/jest/__mocks__/next-link.js',
18
+ '^@akinon/next/hooks$': '<rootDir>/jest/__mocks__/akinon-next-hooks.js',
19
+ '^@akinon/next/hooks/use-localization$':
20
+ '<rootDir>/jest/__mocks__/akinon-next-hooks.js',
21
+ '^@akinon/next/utils$': '<rootDir>/jest/__mocks__/akinon-next-utils.js',
22
+ '^@akinon/next/localization$':
23
+ '<rootDir>/jest/__mocks__/akinon-next-localization.js',
24
+ '^@akinon/next/components/image$':
25
+ '<rootDir>/jest/__mocks__/akinon-next-image.js',
26
+ '^@akinon/next/components/modal$':
27
+ '<rootDir>/jest/__mocks__/akinon-next-modal.js',
28
+ '^@akinon/next/components/price$':
29
+ '<rootDir>/jest/__mocks__/akinon-next-price.js',
30
+ '^@akinon/next/components/loader-spinner$':
31
+ '<rootDir>/jest/__mocks__/akinon-next-loader-spinner.js',
32
+ '^@akinon/next/data/client/(.*)$':
33
+ '<rootDir>/jest/__mocks__/akinon-next-rtk.js'
13
34
  },
14
35
  transform: {
15
36
  '^.+\\.(ts|tsx)$': [
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@akinon/pz-theme",
3
3
  "description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
4
- "version": "2.0.30-beta.0",
4
+ "version": "2.0.30",
5
5
  "license": "MIT",
6
6
  "main": "src/index.ts",
7
7
  "scripts": {
8
8
  "test": "jest"
9
9
  },
10
10
  "peerDependencies": {
11
- "@akinon/next": "2.0.30-beta.0",
11
+ "@akinon/next": "2.0.30",
12
12
  "react": "^18.0.0 || ^19.0.0",
13
13
  "react-dom": "^18.0.0 || ^19.0.0"
14
14
  },
@@ -18,7 +18,10 @@
18
18
  "tailwind-merge": "^2.5.4"
19
19
  },
20
20
  "devDependencies": {
21
- "@akinon/next": "2.0.30-beta.0",
21
+ "@akinon/next": "2.0.30",
22
+ "@testing-library/jest-dom": "5.16.4",
23
+ "@testing-library/react": "16.3.2",
24
+ "@testing-library/user-event": "14.1.1",
22
25
  "@types/jest": "29.5.14",
23
26
  "@types/node": "^18.7.8",
24
27
  "@types/react": "^18.0.17",
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+
4
+ // Registry is a DEFAULT export; force an unregistered type.
5
+ jest.mock('../blocks/block-renderer-registry', () => ({
6
+ __esModule: true,
7
+ default: { getRenderer: () => undefined, hasRenderer: () => false }
8
+ }));
9
+ jest.mock('../components/with-designer-features', () => ({
10
+ __esModule: true,
11
+ WithDesignerFeatures: ({ children }: { children: React.ReactNode }) => children
12
+ }));
13
+
14
+ import ThemeBlock from '../theme-block';
15
+
16
+ const props = (over: Record<string, unknown> = {}) =>
17
+ ({
18
+ block: {
19
+ id: 'x',
20
+ type: 'nonexistent-xyz',
21
+ label: 'x',
22
+ properties: {},
23
+ styles: {},
24
+ order: 0,
25
+ hidden: false
26
+ },
27
+ placeholderId: 'p',
28
+ sectionId: 's',
29
+ isDesigner: false,
30
+ ...over
31
+ } as any);
32
+
33
+ describe('ThemeBlock — unknown block type', () => {
34
+ it('renders nothing in production (no debug text leaks to end users)', () => {
35
+ const { container } = render(<ThemeBlock {...props({ isDesigner: false })} />);
36
+ expect(container.textContent).not.toMatch(/Unknown block type/);
37
+ expect(container.innerHTML).toBe('');
38
+ });
39
+
40
+ it('shows the debug hint in the designer', () => {
41
+ const { container } = render(<ThemeBlock {...props({ isDesigner: true })} />);
42
+ expect(container.textContent).toContain(
43
+ 'Unknown block type: nonexistent-xyz'
44
+ );
45
+ });
46
+ });
@@ -0,0 +1,178 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import AccordionBlock from '../accordion-block';
4
+
5
+ jest.mock('../../theme-block', () => ({
6
+ __esModule: true,
7
+ default: ({ block }: any) => <div data-testid="tb" data-id={block.id} />
8
+ }));
9
+
10
+ const props = (over: Record<string, unknown> = {}) =>
11
+ ({
12
+ block: { id: 'acc', type: 'accordion', label: 'acc', blocks: [] },
13
+ placeholderId: 'p',
14
+ sectionId: 's',
15
+ isDesigner: false,
16
+ ...over
17
+ } as any);
18
+
19
+ const child = (over: Record<string, unknown> = {}) => ({
20
+ id: 'c',
21
+ type: 'text',
22
+ label: 'child',
23
+ hidden: false,
24
+ order: 0,
25
+ ...over
26
+ });
27
+
28
+ describe('AccordionBlock', () => {
29
+ it('renders empty placeholder when no heading/content blocks', () => {
30
+ render(<AccordionBlock {...props()} />);
31
+ expect(screen.getByText('No accordion content')).toBeInTheDocument();
32
+ expect(screen.queryAllByTestId('tb')).toHaveLength(0);
33
+ });
34
+
35
+ it('renders heading and content children via ThemeBlock', () => {
36
+ render(
37
+ <AccordionBlock
38
+ {...props({
39
+ block: {
40
+ id: 'acc',
41
+ blocks: [
42
+ {
43
+ id: 'h',
44
+ label: 'Accordion Heading',
45
+ blocks: [child({ id: 'h1' })]
46
+ },
47
+ {
48
+ id: 'co',
49
+ label: 'Accordion Content',
50
+ blocks: [child({ id: 'c1' })]
51
+ }
52
+ ]
53
+ }
54
+ })}
55
+ />
56
+ );
57
+ const ids = screen.getAllByTestId('tb').map((el) => el.getAttribute('data-id'));
58
+ expect(ids).toEqual(['h1', 'c1']);
59
+ });
60
+
61
+ it('drops hidden children when not designer', () => {
62
+ render(
63
+ <AccordionBlock
64
+ {...props({
65
+ isDesigner: false,
66
+ block: {
67
+ id: 'acc',
68
+ blocks: [
69
+ {
70
+ id: 'h',
71
+ label: 'Accordion Heading',
72
+ blocks: [
73
+ child({ id: 'visible', hidden: false }),
74
+ child({ id: 'gone', hidden: true })
75
+ ]
76
+ }
77
+ ]
78
+ }
79
+ })}
80
+ />
81
+ );
82
+ const ids = screen.getAllByTestId('tb').map((el) => el.getAttribute('data-id'));
83
+ expect(ids).toEqual(['visible']);
84
+ });
85
+
86
+ it('keeps hidden children when designer', () => {
87
+ render(
88
+ <AccordionBlock
89
+ {...props({
90
+ isDesigner: true,
91
+ block: {
92
+ id: 'acc',
93
+ blocks: [
94
+ {
95
+ id: 'h',
96
+ label: 'Accordion Heading',
97
+ blocks: [
98
+ child({ id: 'visible', hidden: false }),
99
+ child({ id: 'kept', hidden: true })
100
+ ]
101
+ }
102
+ ]
103
+ }
104
+ })}
105
+ />
106
+ );
107
+ const ids = screen.getAllByTestId('tb').map((el) => el.getAttribute('data-id'));
108
+ expect(ids).toEqual(['visible', 'kept']);
109
+ });
110
+
111
+ it('orders children by ascending order', () => {
112
+ render(
113
+ <AccordionBlock
114
+ {...props({
115
+ block: {
116
+ id: 'acc',
117
+ blocks: [
118
+ {
119
+ id: 'h',
120
+ label: 'Accordion Heading',
121
+ blocks: [
122
+ child({ id: 'second', order: 2 }),
123
+ child({ id: 'first', order: 1 })
124
+ ]
125
+ }
126
+ ]
127
+ }
128
+ })}
129
+ />
130
+ );
131
+ const ids = screen.getAllByTestId('tb').map((el) => el.getAttribute('data-id'));
132
+ expect(ids).toEqual(['first', 'second']);
133
+ });
134
+
135
+ it('is closed by default (gridTemplateRows 0fr)', () => {
136
+ const { container } = render(
137
+ <AccordionBlock
138
+ {...props({
139
+ block: {
140
+ id: 'acc',
141
+ blocks: [
142
+ {
143
+ id: 'co',
144
+ label: 'Accordion Content',
145
+ blocks: [child({ id: 'c1' })]
146
+ }
147
+ ]
148
+ }
149
+ })}
150
+ />
151
+ );
152
+ const grid = container.querySelector('div[style*="grid"]') as HTMLElement;
153
+ expect(grid).toBeTruthy();
154
+ expect(grid.style.gridTemplateRows).toBe('0fr');
155
+ });
156
+
157
+ it('is open when properties.defaultOpen is true (gridTemplateRows 1fr)', () => {
158
+ const { container } = render(
159
+ <AccordionBlock
160
+ {...props({
161
+ block: {
162
+ id: 'acc',
163
+ properties: { defaultOpen: true },
164
+ blocks: [
165
+ {
166
+ id: 'co',
167
+ label: 'Accordion Content',
168
+ blocks: [child({ id: 'c1' })]
169
+ }
170
+ ]
171
+ }
172
+ })}
173
+ />
174
+ );
175
+ const grid = container.querySelector('div[style*="grid"]') as HTMLElement;
176
+ expect(grid.style.gridTemplateRows).toBe('1fr');
177
+ });
178
+ });
@@ -0,0 +1,99 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import ButtonBlock from '../button-block';
4
+
5
+ // useLocalization is globally mocked to return locale: 'en-us'.
6
+ // getResponsiveValue / colorToRgba / resolveThemeCssVariables come from the
7
+ // real ../utils module (not mocked). The Modal mock renders nothing while
8
+ // closed, so only the <a>/<button> is in the DOM by default.
9
+
10
+ const renderBlock = (block: any, extraProps: any = {}) =>
11
+ render(<ButtonBlock block={block} {...extraProps} />);
12
+
13
+ describe('ButtonBlock', () => {
14
+ describe('getLocalizedText', () => {
15
+ it('falls back to "Click Me" when text is undefined', () => {
16
+ renderBlock({ id: 'b1' });
17
+ expect(screen.getByText('Click Me')).toBeInTheDocument();
18
+ });
19
+
20
+ it('renders a plain string text as-is', () => {
21
+ renderBlock({ id: 'b2', properties: { text: 'Buy Now' } });
22
+ expect(screen.getByText('Buy Now')).toBeInTheDocument();
23
+ });
24
+
25
+ it('renders the active locale value from a localized object (en-us)', () => {
26
+ renderBlock({
27
+ id: 'b3',
28
+ properties: { text: { 'en-us': 'Hello', 'tr-tr': 'Merhaba' } }
29
+ });
30
+ expect(screen.getByText('Hello')).toBeInTheDocument();
31
+ expect(screen.queryByText('Merhaba')).not.toBeInTheDocument();
32
+ });
33
+
34
+ it('parses a JSON-string localized object', () => {
35
+ renderBlock({
36
+ id: 'b3b',
37
+ properties: { text: '{"en-us":"Subscribe","tr-tr":"Abone"}' }
38
+ });
39
+ expect(screen.getByText('Subscribe')).toBeInTheDocument();
40
+ });
41
+
42
+ it('resolves a responsive object via the current breakpoint', () => {
43
+ const block = {
44
+ id: 'b4',
45
+ properties: { text: { desktop: 'Desktop Text', mobile: 'Mobile Text' } }
46
+ };
47
+ renderBlock(block, { currentBreakpoint: 'mobile' });
48
+ expect(screen.getByText('Mobile Text')).toBeInTheDocument();
49
+ });
50
+
51
+ it('falls back to the desktop value of a responsive object by default', () => {
52
+ renderBlock({
53
+ id: 'b5',
54
+ properties: { text: { desktop: 'Desktop Text', mobile: 'Mobile Text' } }
55
+ });
56
+ expect(screen.getByText('Desktop Text')).toBeInTheDocument();
57
+ });
58
+ });
59
+
60
+ describe('tag / href defaults', () => {
61
+ it('renders an <a> with href "#" by default', () => {
62
+ renderBlock({ id: 'b6', properties: { text: 'Link' } });
63
+ const anchor = screen.getByText('Link').closest('a');
64
+ expect(anchor).toBeInTheDocument();
65
+ expect(anchor).toHaveAttribute('href', '#');
66
+ expect(anchor).toHaveAttribute('target', '_self');
67
+ });
68
+
69
+ it('uses the provided url for the anchor href', () => {
70
+ renderBlock({
71
+ id: 'b7',
72
+ properties: { text: 'Go', url: 'https://example.com' }
73
+ });
74
+ const anchor = screen.getByText('Go').closest('a');
75
+ expect(anchor).toHaveAttribute('href', 'https://example.com');
76
+ });
77
+
78
+ it('renders a <button> when tag is "button"', () => {
79
+ renderBlock({
80
+ id: 'b8',
81
+ properties: { text: 'Submit', tag: 'button' }
82
+ });
83
+ const btn = screen.getByRole('button');
84
+ expect(btn).toBeInTheDocument();
85
+ expect(btn.tagName).toBe('BUTTON');
86
+ expect(btn).toHaveAttribute('type', 'button');
87
+ expect(screen.getByText('Submit')).toBeInTheDocument();
88
+ });
89
+
90
+ it('sets rel noopener noreferrer when target is _blank', () => {
91
+ renderBlock({
92
+ id: 'b9',
93
+ properties: { text: 'External', target: '_blank' }
94
+ });
95
+ const anchor = screen.getByText('External').closest('a');
96
+ expect(anchor).toHaveAttribute('rel', 'noopener noreferrer');
97
+ });
98
+ });
99
+ });
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+
4
+ // Stub the recursive ThemeBlock (default export) with a countable marker.
5
+ jest.mock('../../theme-block', () => ({
6
+ __esModule: true,
7
+ default: ({ block }: { block: { id: string } }) => (
8
+ <div data-testid="tb" data-id={block.id} />
9
+ )
10
+ }));
11
+
12
+ import CounterBlock from '../counter-block';
13
+
14
+ const child = (id: string, hidden = false, order = 0) => ({
15
+ id,
16
+ type: 'text',
17
+ label: id,
18
+ value: '',
19
+ properties: {},
20
+ styles: {},
21
+ order,
22
+ hidden
23
+ });
24
+
25
+ const props = (over: Record<string, unknown> = {}) =>
26
+ ({
27
+ block: {
28
+ id: 'c',
29
+ type: 'counter',
30
+ label: 'c',
31
+ value: '',
32
+ properties: {}, // no endAt -> no timers in this test
33
+ styles: {},
34
+ order: 0,
35
+ hidden: false,
36
+ blocks: [child('c1', false), child('c2', true)]
37
+ },
38
+ placeholderId: 'p',
39
+ sectionId: 's',
40
+ isDesigner: false,
41
+ ...over
42
+ } as any);
43
+
44
+ const markerCount = (container: HTMLElement) =>
45
+ container.querySelectorAll('[data-testid="tb"]').length;
46
+
47
+ describe('CounterBlock — hidden children (BUG-1 fix)', () => {
48
+ it('drops hidden children in production', () => {
49
+ const { container } = render(<CounterBlock {...props({ isDesigner: false })} />);
50
+ expect(markerCount(container)).toBe(1);
51
+ });
52
+
53
+ it('keeps hidden children in the designer so they remain selectable', () => {
54
+ const { container } = render(<CounterBlock {...props({ isDesigner: true })} />);
55
+ expect(markerCount(container)).toBe(2);
56
+ });
57
+ });