@ippon-ui/ui 0.0.2
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/.agents/agents/component-library-create-from-pattern-library.agent.md +37 -0
- package/.agents/agents/patten-library-create-component.agent.md +30 -0
- package/.agents/skills/component-library/SKILL.md +169 -0
- package/.agents/skills/pattern-library/SKILL.md +277 -0
- package/.github/workflows/build.yml +34 -0
- package/.gitlab-ci.yml +12 -0
- package/.prettierignore +6 -0
- package/AGENTS.md +50 -0
- package/LICENSE +202 -0
- package/ci/build.yml +15 -0
- package/ci/common.yml +42 -0
- package/ci/deploy.yml +20 -0
- package/icons/LICENCE +202 -0
- package/icons/index.ts +69 -0
- package/icons/package.json +25 -0
- package/icons/tsconfig.json +11 -0
- package/lefthook.yml +10 -0
- package/mise.toml +55 -0
- package/package.json +26 -0
- package/pnpm-workspace.yaml +9 -0
- package/prettier.config.mts +8 -0
- package/react/LICENCE +202 -0
- package/react/README.md +75 -0
- package/react/eslint.config.js +22 -0
- package/react/package.json +63 -0
- package/react/src/CAP.ts +14 -0
- package/react/src/Card.ts +2 -0
- package/react/src/DataSelectable.ts +7 -0
- package/react/src/Grid.ts +33 -0
- package/react/src/IpponBadge.tsx +62 -0
- package/react/src/IpponButton.tsx +93 -0
- package/react/src/IpponButtonCard.tsx +34 -0
- package/react/src/IpponCard.tsx +30 -0
- package/react/src/IpponContainer.tsx +15 -0
- package/react/src/IpponGrid.tsx +56 -0
- package/react/src/IpponHSpace.tsx +56 -0
- package/react/src/IpponIcon.tsx +15 -0
- package/react/src/IpponImportFile.tsx +128 -0
- package/react/src/IpponIon.tsx +45 -0
- package/react/src/IpponMeter.tsx +43 -0
- package/react/src/IpponProgress.tsx +45 -0
- package/react/src/IpponText.tsx +56 -0
- package/react/src/IpponTitle.tsx +45 -0
- package/react/src/IpponVSpace.tsx +43 -0
- package/react/src/Optional.ts +177 -0
- package/react/src/Tokens.ts +36 -0
- package/react/src/index.ts +16 -0
- package/react/test/File.fixture.ts +13 -0
- package/react/test/IpponBadge.spec.tsx +245 -0
- package/react/test/IpponButton.spec.tsx +666 -0
- package/react/test/IpponButtonCard.spec.tsx +162 -0
- package/react/test/IpponCard.spec.tsx +133 -0
- package/react/test/IpponContainer.spec.tsx +56 -0
- package/react/test/IpponGrid.spec.tsx +140 -0
- package/react/test/IpponHSpace.spec.tsx +107 -0
- package/react/test/IpponIcon.spec.tsx +37 -0
- package/react/test/IpponImportFile.spec.tsx +431 -0
- package/react/test/IpponIon.spec.tsx +52 -0
- package/react/test/IpponMeter.spec.tsx +59 -0
- package/react/test/IpponProgress.spec.tsx +68 -0
- package/react/test/IpponText.spec.tsx +149 -0
- package/react/test/IpponTitle.spec.tsx +242 -0
- package/react/test/IpponVSpace.spec.tsx +91 -0
- package/react/tsconfig.app.json +24 -0
- package/react/tsconfig.json +4 -0
- package/react/tsconfig.node.json +23 -0
- package/react/vite.config.ts +30 -0
- package/react/vitest.config.ts +21 -0
- package/styles/.editorconfig +12 -0
- package/styles/.stylelintrc.json +75 -0
- package/styles/LICENCE +202 -0
- package/styles/README.md +107 -0
- package/styles/logo.svg +26 -0
- package/styles/package.json +67 -0
- package/styles/src/atom/_atom.scss +9 -0
- package/styles/src/atom/atom.pug +34 -0
- package/styles/src/atom/badge/_badge.scss +108 -0
- package/styles/src/atom/badge/badge.code.pug +29 -0
- package/styles/src/atom/badge/badge.md +1 -0
- package/styles/src/atom/badge/badge.mixin.pug +24 -0
- package/styles/src/atom/badge/badge.render.pug +7 -0
- package/styles/src/atom/button/_button.scss +242 -0
- package/styles/src/atom/button/button.code.pug +38 -0
- package/styles/src/atom/button/button.md +31 -0
- package/styles/src/atom/button/button.mixin.pug +30 -0
- package/styles/src/atom/button/button.render.pug +13 -0
- package/styles/src/atom/icon/_icon.scss +8 -0
- package/styles/src/atom/icon/icon.code.pug +5 -0
- package/styles/src/atom/icon/icon.md +11 -0
- package/styles/src/atom/icon/icon.mixin.pug +8 -0
- package/styles/src/atom/icon/icon.render.pug +7 -0
- package/styles/src/atom/ion/ion.code.pug +8 -0
- package/styles/src/atom/ion/ion.md +11 -0
- package/styles/src/atom/ion/ion.mixin.pug +8 -0
- package/styles/src/atom/ion/ion.render.pug +7 -0
- package/styles/src/atom/meter/_meter.scss +23 -0
- package/styles/src/atom/meter/meter.code.pug +8 -0
- package/styles/src/atom/meter/meter.md +7 -0
- package/styles/src/atom/meter/meter.mixin.pug +12 -0
- package/styles/src/atom/meter/meter.render.pug +5 -0
- package/styles/src/atom/progress/_progress.scss +23 -0
- package/styles/src/atom/progress/progress.code.pug +8 -0
- package/styles/src/atom/progress/progress.md +7 -0
- package/styles/src/atom/progress/progress.mixin.pug +14 -0
- package/styles/src/atom/progress/progress.render.pug +5 -0
- package/styles/src/atom/tab/_tab.scss +48 -0
- package/styles/src/atom/tab/tab.code.pug +5 -0
- package/styles/src/atom/tab/tab.md +1 -0
- package/styles/src/atom/tab/tab.mixin.pug +14 -0
- package/styles/src/atom/tab/tab.render.pug +4 -0
- package/styles/src/atom/text/_text.scss +74 -0
- package/styles/src/atom/text/text.code.pug +19 -0
- package/styles/src/atom/text/text.md +5 -0
- package/styles/src/atom/text/text.mixin.pug +12 -0
- package/styles/src/atom/text/text.render.pug +7 -0
- package/styles/src/atom/title/_title.scss +68 -0
- package/styles/src/atom/title/title.code.pug +25 -0
- package/styles/src/atom/title/title.md +9 -0
- package/styles/src/atom/title/title.mixin.pug +12 -0
- package/styles/src/atom/title/title.render.pug +5 -0
- package/styles/src/atom/title-display/_title-display.scss +26 -0
- package/styles/src/atom/title-display/title-display.code.pug +9 -0
- package/styles/src/atom/title-display/title-display.md +5 -0
- package/styles/src/atom/title-display/title-display.mixin.pug +6 -0
- package/styles/src/atom/title-display/title-display.render.pug +4 -0
- package/styles/src/doc.scss +2 -0
- package/styles/src/favicon.ico +0 -0
- package/styles/src/function/_conversion.scss +9 -0
- package/styles/src/index.pug +59 -0
- package/styles/src/layout-documentation.pug +14 -0
- package/styles/src/layout.pug +17 -0
- package/styles/src/molecule/_molecule.scss +2 -0
- package/styles/src/molecule/import-file/_import-file.scss +38 -0
- package/styles/src/molecule/import-file/import-file.code.pug +4 -0
- package/styles/src/molecule/import-file/import-file.md +1 -0
- package/styles/src/molecule/import-file/import-file.mixin.pug +15 -0
- package/styles/src/molecule/import-file/import-file.render.pug +5 -0
- package/styles/src/molecule/molecule.pug +20 -0
- package/styles/src/molecule/tabs/_tabs.scss +4 -0
- package/styles/src/molecule/tabs/tabs.code.pug +9 -0
- package/styles/src/molecule/tabs/tabs.md +1 -0
- package/styles/src/molecule/tabs/tabs.mixin.pug +4 -0
- package/styles/src/molecule/tabs/tabs.render.pug +4 -0
- package/styles/src/molecule/toggle/_toggle.scss +68 -0
- package/styles/src/molecule/toggle/toggle.code.pug +26 -0
- package/styles/src/molecule/toggle/toggle.md +1 -0
- package/styles/src/molecule/toggle/toggle.mixin.pug +36 -0
- package/styles/src/molecule/toggle/toggle.render.pug +5 -0
- package/styles/src/organism/_abstract-card.scss +36 -0
- package/styles/src/organism/_docorganism.scss +1 -0
- package/styles/src/organism/_organism.scss +8 -0
- package/styles/src/organism/button-card/_button-card.scss +22 -0
- package/styles/src/organism/button-card/button-card.code.pug +31 -0
- package/styles/src/organism/button-card/button-card.md +28 -0
- package/styles/src/organism/button-card/button-card.mixin.pug +8 -0
- package/styles/src/organism/button-card/button-card.render.pug +7 -0
- package/styles/src/organism/card/_card.scss +6 -0
- package/styles/src/organism/card/card.code.pug +9 -0
- package/styles/src/organism/card/card.md +23 -0
- package/styles/src/organism/card/card.mixin.pug +7 -0
- package/styles/src/organism/card/card.render.pug +7 -0
- package/styles/src/organism/container/_container.scss +3 -0
- package/styles/src/organism/container/container.code.pug +13 -0
- package/styles/src/organism/container/container.md +5 -0
- package/styles/src/organism/container/container.mixin.pug +3 -0
- package/styles/src/organism/container/container.render.pug +4 -0
- package/styles/src/organism/grid/_docgrid.scss +11 -0
- package/styles/src/organism/grid/_grid.scss +84 -0
- package/styles/src/organism/grid/grid.code.pug +25 -0
- package/styles/src/organism/grid/grid.md +1 -0
- package/styles/src/organism/grid/grid.mixin.pug +7 -0
- package/styles/src/organism/grid/grid.render.pug +5 -0
- package/styles/src/organism/h-space/_h-space.scss +49 -0
- package/styles/src/organism/h-space/h-space.code.pug +56 -0
- package/styles/src/organism/h-space/h-space.md +22 -0
- package/styles/src/organism/h-space/h-space.mixin.pug +14 -0
- package/styles/src/organism/h-space/h-space.render.pug +5 -0
- package/styles/src/organism/header/_header.scss +8 -0
- package/styles/src/organism/header/header.code.pug +14 -0
- package/styles/src/organism/header/header.md +1 -0
- package/styles/src/organism/header/header.mixin.pug +7 -0
- package/styles/src/organism/header/header.render.pug +4 -0
- package/styles/src/organism/modal/_modal.scss +58 -0
- package/styles/src/organism/modal/modal.code.pug +68 -0
- package/styles/src/organism/modal/modal.md +1 -0
- package/styles/src/organism/modal/modal.mixin.pug +25 -0
- package/styles/src/organism/modal/modal.render.pug +4 -0
- package/styles/src/organism/organism.pug +30 -0
- package/styles/src/organism/v-space/_v-space.scss +45 -0
- package/styles/src/organism/v-space/v-space.code.pug +41 -0
- package/styles/src/organism/v-space/v-space.md +20 -0
- package/styles/src/organism/v-space/v-space.mixin.pug +7 -0
- package/styles/src/organism/v-space/v-space.render.pug +5 -0
- package/styles/src/quark/_breakpoint.scss +12 -0
- package/styles/src/quark/_font.scss +38 -0
- package/styles/src/quark/_gap.scss +34 -0
- package/styles/src/quark/_placeholder.scss +27 -0
- package/styles/src/quark/_shadow.scss +13 -0
- package/styles/src/quark/_typography.scss +146 -0
- package/styles/src/template/_template.scss +1 -0
- package/styles/src/template/layout/_layout.scss +20 -0
- package/styles/src/template/layout/layout.code.pug +11 -0
- package/styles/src/template/layout/layout.md +1 -0
- package/styles/src/template/layout/layout.mixin.pug +11 -0
- package/styles/src/template/layout/layout.render.pug +4 -0
- package/styles/src/template/template.pug +16 -0
- package/styles/src/tikui.scss +5 -0
- package/styles/src/token/_doctable.scss +14 -0
- package/styles/src/token/_doctoken.scss +1 -0
- package/styles/src/token/_size.scss +9 -0
- package/styles/src/token/_token.scss +5 -0
- package/styles/src/token/color/_color.scss +9 -0
- package/styles/src/token/color/color/_base.scss +65 -0
- package/styles/src/token/color/color/_brand.scss +13 -0
- package/styles/src/token/color/color/_error.scss +13 -0
- package/styles/src/token/color/color/_information-2.scss +13 -0
- package/styles/src/token/color/color/_information.scss +13 -0
- package/styles/src/token/color/color/_neutral.scss +20 -0
- package/styles/src/token/color/color/_semantic.scss +69 -0
- package/styles/src/token/color/color/_success.scss +13 -0
- package/styles/src/token/color/color/_warning.scss +13 -0
- package/styles/src/token/color/color.js +31 -0
- package/styles/src/token/color/color.mixin.pug +19 -0
- package/styles/src/token/color/color.pug +9 -0
- package/styles/src/token/color/color.render.pug +13 -0
- package/styles/src/token/radius/_radius.scss +8 -0
- package/styles/src/token/radius/radius.js +54 -0
- package/styles/src/token/radius/radius.mixin.pug +14 -0
- package/styles/src/token/radius/radius.pug +9 -0
- package/styles/src/token/radius/radius.render.pug +11 -0
- package/styles/src/token/shadow/_shadow.scss +22 -0
- package/styles/src/token/shadow/shadow.js +45 -0
- package/styles/src/token/shadow/shadow.mixin.pug +13 -0
- package/styles/src/token/shadow/shadow.pug +9 -0
- package/styles/src/token/shadow/shadow.render.pug +9 -0
- package/styles/src/token/token.js +38 -0
- package/styles/src/token/token.pug +25 -0
- package/styles/src/token/typography/_typography.scss +103 -0
- package/styles/src/token/typography/typography.js +32 -0
- package/styles/src/token/typography/typography.mixin.pug +17 -0
- package/styles/src/token/typography/typography.pug +9 -0
- package/styles/src/token/typography/typography.render.pug +19 -0
- package/styles/test/function/conversion.test.scss +20 -0
- package/styles/test/function/sass.spec.ts +6 -0
- package/styles/tikuiconfig.json +14 -0
- package/styles/tsconfig.json +10 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { act, render, screen, configure, cleanup } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom/vitest';
|
|
4
|
+
import { IpponButton } from '../src';
|
|
5
|
+
|
|
6
|
+
configure({
|
|
7
|
+
testIdAttribute: 'data-selector',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const getIpponButton = () => screen.getByTestId('ippon-button');
|
|
11
|
+
|
|
12
|
+
const expectToHaveClasses = (...classes: string[]) =>
|
|
13
|
+
expect(getIpponButton()).toHaveClass('ippon-button', ...classes);
|
|
14
|
+
|
|
15
|
+
const expectNotToHaveClasses = (...classes: string[]) => {
|
|
16
|
+
const button = getIpponButton();
|
|
17
|
+
classes.forEach((cls) => expect(button).not.toHaveClass(cls));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const expectToHaveTextContent = (text: string) => expect(getIpponButton()).toHaveTextContent(text);
|
|
21
|
+
|
|
22
|
+
const getTextPart = () => getIpponButton().querySelector('.ippon-button--text');
|
|
23
|
+
|
|
24
|
+
const expectToHaveTextPart = () => expect(getTextPart()).toBeInTheDocument();
|
|
25
|
+
|
|
26
|
+
const getRightIcon = () => getIpponButton().querySelector('.ippon-button--icon:last-of-type');
|
|
27
|
+
|
|
28
|
+
const getLeftIcon = () => getIpponButton().querySelector('.ippon-button--icon:first-of-type');
|
|
29
|
+
|
|
30
|
+
const getIconParts = () => getIpponButton().querySelectorAll<HTMLElement>('.ippon-button--icon');
|
|
31
|
+
|
|
32
|
+
const expectIconPartsCount = (count: number) => expect(getIconParts()).toHaveLength(count);
|
|
33
|
+
|
|
34
|
+
describe('IpponButton', () => {
|
|
35
|
+
afterEach(cleanup);
|
|
36
|
+
|
|
37
|
+
it('should be minimal', () => {
|
|
38
|
+
render(<IpponButton dataSelector="ippon-button">Default</IpponButton>);
|
|
39
|
+
|
|
40
|
+
expectToHaveClasses();
|
|
41
|
+
expectToHaveTextContent('Default');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should click', () => {
|
|
45
|
+
const onClick = vi.fn();
|
|
46
|
+
|
|
47
|
+
render(
|
|
48
|
+
<IpponButton onClick={onClick} dataSelector="ippon-button">
|
|
49
|
+
Click me
|
|
50
|
+
</IpponButton>,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
getIpponButton().click();
|
|
54
|
+
|
|
55
|
+
expect(onClick).toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('Variant', () => {
|
|
59
|
+
it('should be secondary', () => {
|
|
60
|
+
render(
|
|
61
|
+
<IpponButton variant="secondary" dataSelector="ippon-button">
|
|
62
|
+
Secondary
|
|
63
|
+
</IpponButton>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expectToHaveClasses('-secondary');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should be outline', () => {
|
|
70
|
+
render(
|
|
71
|
+
<IpponButton variant="outline" dataSelector="ippon-button">
|
|
72
|
+
Outline
|
|
73
|
+
</IpponButton>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expectToHaveClasses('-outline');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should be text', () => {
|
|
80
|
+
render(
|
|
81
|
+
<IpponButton variant="text" dataSelector="ippon-button">
|
|
82
|
+
Text
|
|
83
|
+
</IpponButton>,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expectToHaveClasses('-text');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('Color', () => {
|
|
91
|
+
it.each(['success', 'error', 'information', 'warning', 'neutral'] as const)(
|
|
92
|
+
'should be %s',
|
|
93
|
+
(color) => {
|
|
94
|
+
render(
|
|
95
|
+
<IpponButton color={color} dataSelector="ippon-button">
|
|
96
|
+
{color}
|
|
97
|
+
</IpponButton>,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expectToHaveClasses(`-${color}`);
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('Size', () => {
|
|
106
|
+
it('should not have size class by default', () => {
|
|
107
|
+
render(<IpponButton dataSelector="ippon-button">Default</IpponButton>);
|
|
108
|
+
|
|
109
|
+
expectNotToHaveClasses('-small', '-large');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it.each(['small', 'large'] as const)('should be %s', (size) => {
|
|
113
|
+
render(
|
|
114
|
+
<IpponButton size={size} dataSelector="ippon-button">
|
|
115
|
+
{size}
|
|
116
|
+
</IpponButton>,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expectToHaveClasses(`-${size}`);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('Disabled', () => {
|
|
124
|
+
it('should not be disabled by default', () => {
|
|
125
|
+
render(<IpponButton dataSelector="ippon-button">Default</IpponButton>);
|
|
126
|
+
|
|
127
|
+
expect(getIpponButton()).not.toBeDisabled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should be disabled', () => {
|
|
131
|
+
render(
|
|
132
|
+
<IpponButton disabled dataSelector="ippon-button">
|
|
133
|
+
Disabled
|
|
134
|
+
</IpponButton>,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(getIpponButton()).toBeDisabled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should not call onClick when disabled', () => {
|
|
141
|
+
const onClick = vi.fn();
|
|
142
|
+
|
|
143
|
+
render(
|
|
144
|
+
<IpponButton disabled onClick={onClick} dataSelector="ippon-button">
|
|
145
|
+
Disabled
|
|
146
|
+
</IpponButton>,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
getIpponButton().click();
|
|
150
|
+
|
|
151
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should be disabled while loading', async () => {
|
|
155
|
+
let resolvePromise: () => void;
|
|
156
|
+
const promise = new Promise<void>((resolve) => {
|
|
157
|
+
resolvePromise = resolve;
|
|
158
|
+
});
|
|
159
|
+
const onClick = () => promise;
|
|
160
|
+
|
|
161
|
+
render(
|
|
162
|
+
<IpponButton onClick={onClick} dataSelector="ippon-button">
|
|
163
|
+
Async
|
|
164
|
+
</IpponButton>,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
act(() => {
|
|
168
|
+
getIpponButton().click();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(getIpponButton()).toBeDisabled();
|
|
172
|
+
|
|
173
|
+
await act(() => {
|
|
174
|
+
resolvePromise!();
|
|
175
|
+
return promise;
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('Icons', () => {
|
|
181
|
+
it('should have icon left', () => {
|
|
182
|
+
render(
|
|
183
|
+
<IpponButton iconLeft={{ name: 'heart' }} dataSelector="ippon-button">
|
|
184
|
+
With left icon
|
|
185
|
+
</IpponButton>,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expectToHaveTextPart();
|
|
189
|
+
expectIconPartsCount(1);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should have icon right', () => {
|
|
193
|
+
render(
|
|
194
|
+
<IpponButton iconRight={{ name: 'search' }} dataSelector="ippon-button">
|
|
195
|
+
With right icon
|
|
196
|
+
</IpponButton>,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
expectToHaveTextPart();
|
|
200
|
+
expectIconPartsCount(1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should have both icons', () => {
|
|
204
|
+
render(
|
|
205
|
+
<IpponButton
|
|
206
|
+
iconLeft={{ name: 'remove' }}
|
|
207
|
+
iconRight={{ name: 'add' }}
|
|
208
|
+
dataSelector="ippon-button"
|
|
209
|
+
>
|
|
210
|
+
With both icons
|
|
211
|
+
</IpponButton>,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expectToHaveTextPart();
|
|
215
|
+
expectIconPartsCount(2);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should not have text part without icons', () => {
|
|
219
|
+
render(<IpponButton dataSelector="ippon-button">No icon</IpponButton>);
|
|
220
|
+
|
|
221
|
+
expect(getTextPart()).not.toBeInTheDocument();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should not have text part with icon but without children', () => {
|
|
225
|
+
render(<IpponButton iconLeft={{ name: 'heart' }} dataSelector="ippon-button" />);
|
|
226
|
+
|
|
227
|
+
expect(getTextPart()).not.toBeInTheDocument();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should have icon when iconLeft is defined without children', () => {
|
|
231
|
+
render(<IpponButton iconLeft={{ name: 'heart' }} dataSelector="ippon-button" />);
|
|
232
|
+
|
|
233
|
+
expectIconPartsCount(1);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should have icon when iconRight is defined without children', () => {
|
|
237
|
+
render(<IpponButton iconRight={{ name: 'search' }} dataSelector="ippon-button" />);
|
|
238
|
+
|
|
239
|
+
expectIconPartsCount(1);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('Loading (async onClick)', () => {
|
|
244
|
+
it('should not be loading by default', () => {
|
|
245
|
+
render(<IpponButton dataSelector="ippon-button">Default</IpponButton>);
|
|
246
|
+
|
|
247
|
+
expectNotToHaveClasses('-loading');
|
|
248
|
+
const ariaValue = getIpponButton().getAttribute('aria-busy');
|
|
249
|
+
expect(ariaValue).toBeNull();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should not be loading with sync onClick', () => {
|
|
253
|
+
const onClick = vi.fn();
|
|
254
|
+
|
|
255
|
+
render(
|
|
256
|
+
<IpponButton onClick={onClick} dataSelector="ippon-button">
|
|
257
|
+
Sync
|
|
258
|
+
</IpponButton>,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
getIpponButton().click();
|
|
262
|
+
|
|
263
|
+
expectNotToHaveClasses('-loading');
|
|
264
|
+
const ariaValue = getIpponButton().getAttribute('aria-busy');
|
|
265
|
+
expect(ariaValue).toBeNull();
|
|
266
|
+
expect(getIpponButton()).not.toBeDisabled();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should be loading during async onClick', async () => {
|
|
270
|
+
let resolvePromise: () => void;
|
|
271
|
+
const promise = new Promise<void>((resolve) => {
|
|
272
|
+
resolvePromise = resolve;
|
|
273
|
+
});
|
|
274
|
+
const onClick = () => promise;
|
|
275
|
+
|
|
276
|
+
render(
|
|
277
|
+
<IpponButton onClick={onClick} dataSelector="ippon-button">
|
|
278
|
+
Async
|
|
279
|
+
</IpponButton>,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
act(() => {
|
|
283
|
+
getIpponButton().click();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expectToHaveClasses('-loading');
|
|
287
|
+
expect(getIpponButton()).toHaveAttribute('aria-busy', 'true');
|
|
288
|
+
expect(getIpponButton()).toBeDisabled();
|
|
289
|
+
|
|
290
|
+
await act(() => {
|
|
291
|
+
resolvePromise!();
|
|
292
|
+
return promise;
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
expectNotToHaveClasses('-loading');
|
|
296
|
+
const ariaValue = getIpponButton().getAttribute('aria-busy');
|
|
297
|
+
expect(ariaValue).toBeNull();
|
|
298
|
+
expect(getIpponButton()).not.toBeDisabled();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should stop loading after async onClick rejection', async () => {
|
|
302
|
+
let rejectPromise: () => void;
|
|
303
|
+
const promise = new Promise<void>((_, reject) => {
|
|
304
|
+
rejectPromise = reject;
|
|
305
|
+
});
|
|
306
|
+
const onClick = () => promise;
|
|
307
|
+
|
|
308
|
+
render(
|
|
309
|
+
<IpponButton onClick={onClick} dataSelector="ippon-button">
|
|
310
|
+
Async error
|
|
311
|
+
</IpponButton>,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
act(() => {
|
|
315
|
+
getIpponButton().click();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
expectToHaveClasses('-loading');
|
|
319
|
+
|
|
320
|
+
await act(async () => {
|
|
321
|
+
rejectPromise!();
|
|
322
|
+
try {
|
|
323
|
+
await promise;
|
|
324
|
+
} catch {
|
|
325
|
+
// Expected rejection
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
expectNotToHaveClasses('-loading');
|
|
330
|
+
const ariaValue = getIpponButton().getAttribute('aria-busy');
|
|
331
|
+
expect(ariaValue).toBeNull();
|
|
332
|
+
expect(getIpponButton()).not.toBeDisabled();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should not trigger onClick while loading', async () => {
|
|
336
|
+
let resolvePromise: () => void;
|
|
337
|
+
const promise = new Promise<void>((resolve) => {
|
|
338
|
+
resolvePromise = resolve;
|
|
339
|
+
});
|
|
340
|
+
const onClick = vi.fn(() => promise);
|
|
341
|
+
|
|
342
|
+
render(
|
|
343
|
+
<IpponButton onClick={onClick} dataSelector="ippon-button">
|
|
344
|
+
Async
|
|
345
|
+
</IpponButton>,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
act(() => {
|
|
349
|
+
getIpponButton().click();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
353
|
+
|
|
354
|
+
act(() => {
|
|
355
|
+
getIpponButton().click();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
359
|
+
|
|
360
|
+
await act(() => {
|
|
361
|
+
resolvePromise!();
|
|
362
|
+
return promise;
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should handle click without onClick callback', () => {
|
|
367
|
+
render(<IpponButton dataSelector="ippon-button">No callback</IpponButton>);
|
|
368
|
+
|
|
369
|
+
// Should not throw or cause any issues
|
|
370
|
+
expect(() => {
|
|
371
|
+
getIpponButton().click();
|
|
372
|
+
}).not.toThrow();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should handle onClick that returns void (not a promise)', () => {
|
|
376
|
+
const onClick = vi.fn(() => {
|
|
377
|
+
// Return void, not a promise
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
render(
|
|
381
|
+
<IpponButton onClick={onClick} dataSelector="ippon-button">
|
|
382
|
+
Sync void
|
|
383
|
+
</IpponButton>,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
getIpponButton().click();
|
|
387
|
+
|
|
388
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
389
|
+
expectNotToHaveClasses('-loading');
|
|
390
|
+
const ariaValue = getIpponButton().getAttribute('aria-busy');
|
|
391
|
+
expect(ariaValue).toBeNull();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should not be clickable while loading from previous async call', async () => {
|
|
395
|
+
let resolvePromise: () => void;
|
|
396
|
+
const promise = new Promise<void>((resolve) => {
|
|
397
|
+
resolvePromise = resolve;
|
|
398
|
+
});
|
|
399
|
+
const onClick = vi.fn(() => promise);
|
|
400
|
+
|
|
401
|
+
render(
|
|
402
|
+
<IpponButton onClick={onClick} dataSelector="ippon-button">
|
|
403
|
+
Async button
|
|
404
|
+
</IpponButton>,
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
act(() => {
|
|
408
|
+
getIpponButton().click();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
expectToHaveClasses('-loading');
|
|
412
|
+
expect(getIpponButton()).toBeDisabled();
|
|
413
|
+
|
|
414
|
+
// Try to click while loading
|
|
415
|
+
act(() => {
|
|
416
|
+
getIpponButton().click();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Should still only have been called once
|
|
420
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
421
|
+
|
|
422
|
+
await act(() => {
|
|
423
|
+
resolvePromise!();
|
|
424
|
+
return promise;
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
expectNotToHaveClasses('-loading');
|
|
428
|
+
expect(getIpponButton()).not.toBeDisabled();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('should replace right icon with sync icon during loading', async () => {
|
|
432
|
+
let resolvePromise: () => void;
|
|
433
|
+
const promise = new Promise<void>((resolve) => {
|
|
434
|
+
resolvePromise = resolve;
|
|
435
|
+
});
|
|
436
|
+
const onClick = () => promise;
|
|
437
|
+
|
|
438
|
+
render(
|
|
439
|
+
<IpponButton onClick={onClick} iconRight={{ name: 'add' }} dataSelector="ippon-button">
|
|
440
|
+
Async
|
|
441
|
+
</IpponButton>,
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
const rightIcon = getRightIcon();
|
|
445
|
+
expect(rightIcon).toHaveClass('ippon-ion-add');
|
|
446
|
+
expect(rightIcon).not.toHaveClass('-loading');
|
|
447
|
+
|
|
448
|
+
act(() => {
|
|
449
|
+
getIpponButton().click();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
const loadingRightIcon = getRightIcon();
|
|
453
|
+
expect(loadingRightIcon).toHaveClass('ippon-ion-sync');
|
|
454
|
+
expect(loadingRightIcon).toHaveClass('-loading');
|
|
455
|
+
expect(loadingRightIcon).not.toHaveClass('ippon-ion-add');
|
|
456
|
+
|
|
457
|
+
await act(() => {
|
|
458
|
+
resolvePromise!();
|
|
459
|
+
return promise;
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const restoredRightIcon = getRightIcon();
|
|
463
|
+
expect(restoredRightIcon).toHaveClass('ippon-ion-add');
|
|
464
|
+
expect(restoredRightIcon).not.toHaveClass('ippon-ion-sync');
|
|
465
|
+
expect(restoredRightIcon).not.toHaveClass('-loading');
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('should not have loading class on left icon during loading', async () => {
|
|
469
|
+
let resolvePromise: () => void;
|
|
470
|
+
const promise = new Promise<void>((resolve) => {
|
|
471
|
+
resolvePromise = resolve;
|
|
472
|
+
});
|
|
473
|
+
const onClick = () => promise;
|
|
474
|
+
|
|
475
|
+
render(
|
|
476
|
+
<IpponButton
|
|
477
|
+
onClick={onClick}
|
|
478
|
+
iconLeft={{ name: 'remove' }}
|
|
479
|
+
iconRight={{ name: 'add' }}
|
|
480
|
+
dataSelector="ippon-button"
|
|
481
|
+
>
|
|
482
|
+
Async
|
|
483
|
+
</IpponButton>,
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
act(() => {
|
|
487
|
+
getIpponButton().click();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const leftIcon = getLeftIcon();
|
|
491
|
+
expect(leftIcon).toHaveClass('ippon-ion-remove');
|
|
492
|
+
expect(leftIcon).not.toHaveClass('-loading');
|
|
493
|
+
|
|
494
|
+
const loadingRightIcon = getRightIcon();
|
|
495
|
+
expect(loadingRightIcon).toHaveClass('ippon-ion-sync');
|
|
496
|
+
expect(loadingRightIcon).toHaveClass('-loading');
|
|
497
|
+
|
|
498
|
+
await act(() => {
|
|
499
|
+
resolvePromise!();
|
|
500
|
+
return promise;
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('should not replace right icon when not loading', () => {
|
|
505
|
+
render(
|
|
506
|
+
<IpponButton iconRight={{ name: 'add' }} dataSelector="ippon-button">
|
|
507
|
+
Not loading
|
|
508
|
+
</IpponButton>,
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
const rightIcon = getRightIcon();
|
|
512
|
+
expect(rightIcon).toHaveClass('ippon-ion-add');
|
|
513
|
+
expect(rightIcon).not.toHaveClass('ippon-ion-sync');
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('should not add sync icon during loading without right icon', async () => {
|
|
517
|
+
let resolvePromise: () => void;
|
|
518
|
+
const promise = new Promise<void>((resolve) => {
|
|
519
|
+
resolvePromise = resolve;
|
|
520
|
+
});
|
|
521
|
+
const onClick = () => promise;
|
|
522
|
+
|
|
523
|
+
render(
|
|
524
|
+
<IpponButton onClick={onClick} dataSelector="ippon-button">
|
|
525
|
+
Async
|
|
526
|
+
</IpponButton>,
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
act(() => {
|
|
530
|
+
getIpponButton().click();
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
expectIconPartsCount(0);
|
|
534
|
+
|
|
535
|
+
await act(() => {
|
|
536
|
+
resolvePromise!();
|
|
537
|
+
return promise;
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
describe('Combinations', () => {
|
|
543
|
+
it('should combine color, variant and size', () => {
|
|
544
|
+
render(
|
|
545
|
+
<IpponButton color="success" variant="outline" size="large" dataSelector="ippon-button">
|
|
546
|
+
Combined
|
|
547
|
+
</IpponButton>,
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
expectToHaveClasses('-success', '-outline', '-large');
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('should combine color, variant and icon', () => {
|
|
554
|
+
render(
|
|
555
|
+
<IpponButton
|
|
556
|
+
color="error"
|
|
557
|
+
variant="secondary"
|
|
558
|
+
iconLeft={{ name: 'alert-circle', variant: 'outline' }}
|
|
559
|
+
dataSelector="ippon-button"
|
|
560
|
+
>
|
|
561
|
+
Error with icon
|
|
562
|
+
</IpponButton>,
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
expectToHaveClasses('-error', '-secondary');
|
|
566
|
+
expectToHaveTextPart();
|
|
567
|
+
expectIconPartsCount(1);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('should render children without text wrapper when no icon', () => {
|
|
571
|
+
render(<IpponButton dataSelector="ippon-button">No icon text</IpponButton>);
|
|
572
|
+
|
|
573
|
+
// When no icon, children are rendered directly (not wrapped in .ippon-button--text)
|
|
574
|
+
expect(getTextPart()).not.toBeInTheDocument();
|
|
575
|
+
expect(getIpponButton()).toHaveTextContent('No icon text');
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('should render children in text wrapper when icon exists', () => {
|
|
579
|
+
render(
|
|
580
|
+
<IpponButton iconLeft={{ name: 'heart' }} dataSelector="ippon-button">
|
|
581
|
+
With icon text
|
|
582
|
+
</IpponButton>,
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
// When icon exists, children are wrapped in .ippon-button--text
|
|
586
|
+
expectToHaveTextPart();
|
|
587
|
+
expect(getTextPart()).toHaveTextContent('With icon text');
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it('should render both children and text wrapper when both icons exist', () => {
|
|
591
|
+
render(
|
|
592
|
+
<IpponButton
|
|
593
|
+
iconLeft={{ name: 'heart' }}
|
|
594
|
+
iconRight={{ name: 'search' }}
|
|
595
|
+
dataSelector="ippon-button"
|
|
596
|
+
>
|
|
597
|
+
Both icons text
|
|
598
|
+
</IpponButton>,
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
expectToHaveTextPart();
|
|
602
|
+
expect(getTextPart()).toHaveTextContent('Both icons text');
|
|
603
|
+
expectIconPartsCount(2);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('should render icon even without children', () => {
|
|
607
|
+
render(<IpponButton iconLeft={{ name: 'heart' }} dataSelector="ippon-button" />);
|
|
608
|
+
|
|
609
|
+
expectIconPartsCount(1);
|
|
610
|
+
expect(getTextPart()).not.toBeInTheDocument();
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it('should render right icon even without children', () => {
|
|
614
|
+
render(<IpponButton iconRight={{ name: 'search' }} dataSelector="ippon-button" />);
|
|
615
|
+
|
|
616
|
+
expectIconPartsCount(1);
|
|
617
|
+
expect(getTextPart()).not.toBeInTheDocument();
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('should render disabled button with icon', () => {
|
|
621
|
+
render(
|
|
622
|
+
<IpponButton disabled iconLeft={{ name: 'heart' }} dataSelector="ippon-button">
|
|
623
|
+
Disabled with icon
|
|
624
|
+
</IpponButton>,
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
expect(getIpponButton()).toBeDisabled();
|
|
628
|
+
expectToHaveTextPart();
|
|
629
|
+
expectIconPartsCount(1);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('should handle onClick that returns non-Promise object', () => {
|
|
633
|
+
const onClick = vi.fn(() => {
|
|
634
|
+
// Return void (undefined), not a promise
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
render(
|
|
638
|
+
<IpponButton onClick={onClick} dataSelector="ippon-button">
|
|
639
|
+
Non-promise object
|
|
640
|
+
</IpponButton>,
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
getIpponButton().click();
|
|
644
|
+
|
|
645
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
646
|
+
expectNotToHaveClasses('-loading');
|
|
647
|
+
const ariaValue = getIpponButton().getAttribute('aria-busy');
|
|
648
|
+
expect(ariaValue).toBeNull();
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('should handle onClick that returns undefined', () => {
|
|
652
|
+
const onClick = vi.fn(() => undefined);
|
|
653
|
+
|
|
654
|
+
render(
|
|
655
|
+
<IpponButton onClick={onClick} dataSelector="ippon-button">
|
|
656
|
+
Undefined return
|
|
657
|
+
</IpponButton>,
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
getIpponButton().click();
|
|
661
|
+
|
|
662
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
663
|
+
expectNotToHaveClasses('-loading');
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
});
|