@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.
- package/CHANGELOG.md +6 -3
- package/jest/__mocks__/akinon-next-hooks.js +15 -0
- package/jest/__mocks__/akinon-next-image.js +22 -0
- package/jest/__mocks__/akinon-next-loader-spinner.js +7 -0
- package/jest/__mocks__/akinon-next-localization.js +10 -0
- package/jest/__mocks__/akinon-next-modal.js +11 -0
- package/jest/__mocks__/akinon-next-price.js +9 -0
- package/jest/__mocks__/akinon-next-rtk.js +30 -0
- package/jest/__mocks__/akinon-next-utils.js +8 -0
- package/jest/__mocks__/next-link.js +11 -0
- package/jest/__mocks__/settings.js +13 -0
- package/jest/setup.ts +33 -0
- package/jest.config.js +24 -3
- package/package.json +6 -3
- package/src/__tests__/theme-block.test.tsx +46 -0
- package/src/blocks/__tests__/accordion-block.test.tsx +178 -0
- package/src/blocks/__tests__/button-block.test.tsx +99 -0
- package/src/blocks/__tests__/counter-block.test.tsx +57 -0
- package/src/blocks/__tests__/embed-block.test.tsx +89 -0
- package/src/blocks/__tests__/group-block.test.tsx +175 -0
- package/src/blocks/__tests__/hotspot-block.test.tsx +93 -0
- package/src/blocks/__tests__/icon-block.test.tsx +88 -0
- package/src/blocks/__tests__/image-block.test.tsx +139 -0
- package/src/blocks/__tests__/image-gallery-block.test.tsx +117 -0
- package/src/blocks/__tests__/link-block.test.tsx +184 -0
- package/src/blocks/__tests__/map-block.test.tsx +165 -0
- package/src/blocks/__tests__/slider-block.test.tsx +148 -0
- package/src/blocks/__tests__/text-block.test.tsx +87 -0
- package/src/blocks/__tests__/trivial-blocks.test.tsx +59 -0
- package/src/blocks/__tests__/video-block.test.tsx +196 -0
- package/src/blocks/counter-block.tsx +1 -1
- package/src/blocks/group-block.tsx +1 -1
- package/src/blocks/icon-block.tsx +11 -6
- package/src/blocks/video-block.tsx +1 -1
- package/src/sections/__tests__/before-after-section.test.tsx +139 -0
- package/src/sections/__tests__/countdown-campaign-banner-section.test.tsx +188 -0
- package/src/sections/__tests__/coupon-banner-section.test.tsx +188 -0
- package/src/sections/__tests__/divider-section.test.tsx +161 -0
- package/src/sections/__tests__/hover-showcase-section.test.tsx +193 -0
- package/src/sections/__tests__/image-hotspot-section.test.tsx +110 -0
- package/src/sections/__tests__/notification-banner-section.test.tsx +144 -0
- package/src/sections/__tests__/section-wrapper.test.tsx +164 -0
- package/src/sections/__tests__/stats-counter-section.test.tsx +245 -0
- package/src/sections/__tests__/tabs-section.test.tsx +115 -0
- package/src/sections/coupon-banner-section.tsx +1 -4
- 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
|
|
3
|
+
## 2.0.30
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
|
|
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,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
|
-
|
|
10
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
+
});
|