diff --git a/.github/workflows/chromatic-pr.yml b/.github/workflows/chromatic-pr.check.yml similarity index 90% rename from .github/workflows/chromatic-pr.yml rename to .github/workflows/chromatic-pr.check.yml index f955df7..c3df347 100644 --- a/.github/workflows/chromatic-pr.yml +++ b/.github/workflows/chromatic-pr.check.yml @@ -1,4 +1,4 @@ -name: 'Chromatic PR Review' +name: 'Chromatic PR Check' permissions: pull-requests: write @@ -30,11 +30,11 @@ jobs: - name: Install dependencies run: | - yarn install --immutable + yarn install --immutable yarn dlx @yarnpkg/sdks vscode - name: Build Storybook - run: yarn workspace @zagdang/ui build-storybook + run: yarn workspace @zagdang/ui build-storybook --webpack-stats-json - name: Create Preview id: chromatic diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-push.check.yml similarity index 84% rename from .github/workflows/pr-check.yml rename to .github/workflows/pr-push.check.yml index 6b29b1e..7e25ec6 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-push.check.yml @@ -1,4 +1,4 @@ -name: PR and Push Check +name: PR and Push Check Test on: pull_request: @@ -18,7 +18,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: '18' # 프로젝트에서 사용하는 Node.js 버전으로 설정 + node-version: '18' - name: Cache yarn dependencies uses: actions/cache@v3 @@ -41,4 +41,4 @@ jobs: run: yarn format --check - name: Build the project - run: yarn build # 빌드 체크 추가 + run: yarn build diff --git a/.github/workflows/ui-packaging-test.yml b/.github/workflows/ui-packaging-test.yml deleted file mode 100644 index dec6324..0000000 --- a/.github/workflows/ui-packaging-test.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: UI Package Test - -on: - pull_request: - types: [opened, synchronize, reopened] - branches: [main, develop] - -jobs: - test: - runs-on: ubuntu-latest - defaults: - run: - working-directory: packages/ui - - steps: - - uses: actions/checkout@v3 - - - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Setup Yarn - run: | - corepack enable - corepack prepare yarn@4.5.3 --activate - - - name: Install dependencies - run: yarn install --immutable - - - name: Run tests - run: | - yarn add -D jest @testing-library/jest-dom @testing-library/dom @testing-library/react - yarn test \ No newline at end of file diff --git a/.github/workflows/unit.test.yml b/.github/workflows/unit.test.yml new file mode 100644 index 0000000..1c6eb60 --- /dev/null +++ b/.github/workflows/unit.test.yml @@ -0,0 +1,56 @@ +name: UI Unit Test + +permissions: + pull-requests: write + contents: read + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: [main, develop] + +jobs: + test: + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/ui + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Set up Yarn 2 (Berry) + run: | + corepack enable + corepack prepare yarn@4.5.3 --activate + + - name: Install dependencies + run: yarn install --immutable + + - name: Run tests with coverage + run: yarn test --coverage + + - name: Get Coverage Info + id: coverage + run: | + COVERAGE_REPORT=$(cat coverage/coverage-summary.json) + echo "statements=$(echo $COVERAGE_REPORT | jq -r '.total.statements.pct')" >> $GITHUB_OUTPUT + echo "branches=$(echo $COVERAGE_REPORT | jq -r '.total.branches.pct')" >> $GITHUB_OUTPUT + echo "functions=$(echo $COVERAGE_REPORT | jq -r '.total.functions.pct')" >> $GITHUB_OUTPUT + echo "lines=$(echo $COVERAGE_REPORT | jq -r '.total.lines.pct')" >> $GITHUB_OUTPUT + + - name: Comment PR with Coverage + uses: thollander/actions-comment-pull-request@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + message: | + ## Test Coverage Report 📊 + - Statements: ${{ steps.coverage.outputs.statements }}% + - Branches: ${{ steps.coverage.outputs.branches }}% + - Functions: ${{ steps.coverage.outputs.functions }}% + - Lines: ${{ steps.coverage.outputs.lines }}% diff --git a/packages/ui/jest.config.js b/packages/ui/jest.config.js index 6d9fc80..d02e555 100644 --- a/packages/ui/jest.config.js +++ b/packages/ui/jest.config.js @@ -10,5 +10,16 @@ export default { transform: { '^.+\\.(ts|tsx)$': ['ts-jest'], '\\.css$': 'jest-transform-css' + }, + collectCoverage: true, + coverageDirectory: 'coverage', + coverageReporters: ['json-summary', 'text', 'lcov'], + coverageThreshold: { + global: { + branches: 80, + functions: 80, + lines: 80, + statements: 80 + } } } \ No newline at end of file diff --git a/packages/ui/package.json b/packages/ui/package.json index cd44dd9..762d0c6 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -13,7 +13,7 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "test": "jest", - "test:watch": "jest --watch", + "test:watch": "jest --watchAll", "test:coverage": "jest --coverage", "test:ci": "jest --ci", "test:changed": "jest --changedSince=HEAD", diff --git a/packages/ui/src/components/Button/button.test.tsx b/packages/ui/src/components/Button/button.test.tsx index 2c17ba6..cc1b689 100644 --- a/packages/ui/src/components/Button/button.test.tsx +++ b/packages/ui/src/components/Button/button.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import React from 'react'; import { Button } from './button'; @@ -17,7 +18,18 @@ describe('Button', () => { }); it('renders with variant classes', () => { - render(); - expect(screen.getByText('Delete')).toHaveClass('bg-destructive'); + render(); + const button = screen.getByRole('button', { name: 'Default' }); + expect(button.tagName).toBe('BUTTON'); + }); + + it('renders as a custom component when asChild is true', () => { + render( + , + ); + const link = screen.getByRole('link', { name: 'Link' }); + expect(link).toHaveAttribute('href', '/test'); }); }); diff --git a/packages/ui/src/components/Input/input.stories.tsx b/packages/ui/src/components/Input/input.stories.tsx new file mode 100644 index 0000000..a4b6648 --- /dev/null +++ b/packages/ui/src/components/Input/input.stories.tsx @@ -0,0 +1,93 @@ +// Input.stories.tsx +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; + +import { Input } from './input'; + +import { Button } from '@/components'; + +const meta = { + title: 'Components/Input', + component: Input, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + placeholder: 'Enter text...', + }, +}; + +export const Widths: Story = { + decorators: [ + (Story) => ( +
+
+
+

Full width (default)

+ +
+
+

w-96

+ +
+
+

w-72

+ +
+
+

w-1/2

+ +
+
+
+ ), + ], + args: { + placeholder: 'Full width input...', + }, +}; + +export const Disabled: Story = { + args: { + placeholder: 'Disabled input', + disabled: true, + }, +}; + +export const WithValue_Controlled: Story = { + render: () => { + const [value, setValue] = React.useState('Sample text'); + + return ( +
+ setValue(e.target.value)} + placeholder="Type to change..." + /> + +
+ ); + }, +}; + +export const WithType: Story = { + args: { + type: 'password', + placeholder: 'Enter password', + }, +}; + +export const WithError: Story = { + args: { + className: 'border-error', + placeholder: 'Error state', + }, +}; diff --git a/packages/ui/src/components/Input/input.test.tsx b/packages/ui/src/components/Input/input.test.tsx new file mode 100644 index 0000000..3426108 --- /dev/null +++ b/packages/ui/src/components/Input/input.test.tsx @@ -0,0 +1,39 @@ +// Input.test.tsx +import { render, screen, fireEvent } from '@testing-library/react'; +import React from 'react'; + +import { Input } from './input'; + +describe('Input Component', () => { + it('renders correctly', () => { + render(); + const input = screen.getByPlaceholderText('Test input'); + expect(input).toBeInTheDocument(); + }); + + it('accepts value changes', () => { + render(); + const input = screen.getByRole('textbox'); + + fireEvent.change(input, { target: { value: 'Test value' } }); + expect(input).toHaveValue('Test value'); + }); + + it('forwards ref correctly', () => { + const ref = React.createRef(); + render(); + expect(ref.current).toBeInstanceOf(HTMLInputElement); + }); + + it('handles disabled state', () => { + render(); + const input = screen.getByRole('textbox'); + expect(input).toBeDisabled(); + }); + + it('handles different input types', () => { + render(); + const input = screen.getByRole('textbox'); + expect(input).toHaveAttribute('type', 'email'); + }); +}); diff --git a/packages/ui/src/components/Input/input.tsx b/packages/ui/src/components/Input/input.tsx new file mode 100644 index 0000000..1969bb3 --- /dev/null +++ b/packages/ui/src/components/Input/input.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = 'Input'; + +export { Input }; diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 7d2b0dc..6ccf0fa 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -1 +1,2 @@ -export { Button as default } from './Button/button'; +export { Button } from './Button/button'; +export { Input } from './Input/input'; diff --git a/packages/ui/tailwind.config.js b/packages/ui/tailwind.config.js index 54449c5..620886f 100644 --- a/packages/ui/tailwind.config.js +++ b/packages/ui/tailwind.config.js @@ -40,6 +40,7 @@ export default { DEFAULT: 'hsl(var(--brand-600))', foreground: 'hsl(var(--brand-600-foreground))', // error-foreground가 추가되었을 경우 }, + error: 'hsl(var(--error))', border: 'hsl(var(--border))', input: 'hsl(var(--input))', ring: 'hsl(var(--ring))', diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts index 3bd6c73..47947fb 100644 --- a/packages/ui/vite.config.ts +++ b/packages/ui/vite.config.ts @@ -20,5 +20,10 @@ export default defineConfig({ rollupOptions: { external: ['react', 'react-dom'], }, + + sourcemap: true, + reportCompressedSize: false, + chunkSizeWarningLimit: 1000, + manifest: true, }, });