diff --git a/.cursor/rules/coding.mdc b/.cursor/rules/coding.mdc index d844f05..9db6045 100644 --- a/.cursor/rules/coding.mdc +++ b/.cursor/rules/coding.mdc @@ -11,6 +11,13 @@ alwaysApply: true - Make sure you understand docs/planning.md and docs/project_plan.md - Before starting coding, always present a plan for approval. - Tasks are in docs/project_plan.md -- Each task will be implemented via a feature branch and a PR. -- Before PR is submitted, the task is marked with "PR" in docs/project_plan.md. -- After task is completed (PR merged), the task is marked with a checkmark in docs/project_plan.md +- Each task will be implemented via a feature branch and a PR to the develop branch. +- Always follow the following recipe for implementing tasks: + 1. Start implementing after the user has explicitly told so. + 2. After finishing implementing, perform all necessary tests and check if the DoD for this task is met. + 3. Report on test results and DoD criteria and ask user if a PR should be submitted. + 4. If the user approves submitting a PR, and before PR is submitted, the task is marked with "PR" in docs/project_plan.md. + 5. After PR ist submitted, report to the user and wait for manual instructions. + 6. The user will then review and merge the PR or ask for updates. + 7. Pull the repo again to check if the PR has been merged. + 8. After task is completed (PR merged), the task is marked with a checkmark in docs/project_plan.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2896f55..090de5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [main] + branches: [main, develop] pull_request: - branches: [main] + branches: [main, develop] jobs: install: diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml index 9146188..01d2e50 100644 --- a/.github/workflows/docker-test.yml +++ b/.github/workflows/docker-test.yml @@ -2,9 +2,9 @@ name: Docker Test on: push: - branches: [main] + branches: [main, develop] pull_request: - branches: [main] + branches: [main, develop] jobs: build-and-test: diff --git a/docs/project_plan.md b/docs/project_plan.md index 17897e0..0f151b0 100644 --- a/docs/project_plan.md +++ b/docs/project_plan.md @@ -15,6 +15,7 @@ A pragmatic breakdown into **four one‑week sprints** plus a preparatory **Spri | 0.4 | Commit Husky hooks (commitlint, lint‑staged). | Attempting to commit code with ESLint errors is blocked locally. | ✓ | | 0.5 | Seed Changesets & automatic versioning. | Merging PR increments `package.json version` and creates a changelog file. | ✓ | | 0.6 | **Docker/Dokku infra** – Add multi‑stage `Dockerfile`, `Procfile`; CI job builds image & pushes to test Dokku app. | `gh workflow run docker-test` builds & deploys; Dokku reports container running, health‑check 200. | ✓ | +| 0.7 | **Update GitHub Actions** – Configure CI workflows to run on develop branch and PRs. | CI workflows run on both main and develop branches, as well as PRs targeting these branches. | PR | --- @@ -32,7 +33,7 @@ A pragmatic breakdown into **four one‑week sprints** plus a preparatory **Spri | 1.2b | Install and configure Storybook addon‑docs. | Documentation tab shows component documentation. | ✓ | | 1.2c | Configure and verify Storybook addon‑a11y. | axe‑a11y addon shows zero violations. | ✓ | | 1.3 | Add global theme bridging (`theme.css` ↔ DaisyUI). | Cypress visual diff (light/dark) matches golden images. | ✓ | -| 1.4 | Create first **AuthShell** layout with logo slot. | Rendered via Storybook; Playwright snapshot approved. | | +| 1.4 | Create first **AuthShell** layout with logo slot. | Rendered via Storybook; Playwright snapshot approved. | PR | | 1.5 | Document design tokens (`DESIGN_TOKENS.md`). | File exists; CI step `tokens-check` verifies presence of each CSS var. | | --- diff --git a/packages/ui-kit/package.json b/packages/ui-kit/package.json index f3d6480..cf7c87e 100644 --- a/packages/ui-kit/package.json +++ b/packages/ui-kit/package.json @@ -27,7 +27,10 @@ "build-storybook": "storybook build", "cy:open": "cypress open", "cy:run": "cypress run", - "theme-screenshots": "cypress run --spec \"cypress/e2e/theme.cy.ts\"" + "theme-screenshots": "cypress run --spec \"cypress/e2e/theme.cy.ts\"", + "playwright:install": "playwright install", + "playwright:test": "playwright test", + "playwright:update-snapshots": "playwright test --update-snapshots" }, "dependencies": { "@radix-ui/react-slot": "^1.0.2", @@ -43,6 +46,7 @@ "devDependencies": { "@chromatic-com/storybook": "^1.9.0", "@eslint/js": "^9.27.0", + "@playwright/test": "^1.52.0", "@storybook/addon-a11y": "^8.6.14", "@storybook/addon-docs": "^8.6.14", "@storybook/addon-essentials": "^8.6.14", diff --git a/packages/ui-kit/playwright.config.ts b/packages/ui-kit/playwright.config.ts new file mode 100644 index 0000000..395e26a --- /dev/null +++ b/packages/ui-kit/playwright.config.ts @@ -0,0 +1,31 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + timeout: 30000, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + + use: { + baseURL: 'http://localhost:6006', // Storybook URL + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'pnpm run storybook', + url: 'http://localhost:6006', + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, +}); \ No newline at end of file diff --git a/packages/ui-kit/src/layout/AuthShell/AuthShell.stories.tsx b/packages/ui-kit/src/layout/AuthShell/AuthShell.stories.tsx new file mode 100644 index 0000000..76b9203 --- /dev/null +++ b/packages/ui-kit/src/layout/AuthShell/AuthShell.stories.tsx @@ -0,0 +1,102 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { AuthShell } from './AuthShell'; +import { Button } from '@/components/primitives/Button'; + +const meta: Meta = { + title: 'Layout/AuthShell', + component: AuthShell, + parameters: { + layout: 'fullscreen', + a11y: { + disable: false, + }, + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +// Sample logo component for the stories +const Logo = () => ( +
+ Brand + Logo +
+); + +// Basic login form for the stories +const LoginForm = () => ( +
+
Sign in to your account
+
+ + +
+
+ + +
+
+ +
+
+); + +/** + * Default AuthShell with logo and login form + */ +export const Default: Story = { + args: { + logo: , + children: , + width: 'md', + }, +}; + +/** + * AuthShell with logo, login form, and footer text + */ +export const WithFooter: Story = { + args: { + logo: , + children: , + footer: ( +
+

Don't have an account? Sign up

+

© 2023 BrandName. All rights reserved.

+
+ ), + width: 'md', + }, +}; + +/** + * Small variant of the AuthShell + */ +export const Small: Story = { + args: { + logo: , + children: , + width: 'sm', + }, +}; + +/** + * Large variant of the AuthShell + */ +export const Large: Story = { + args: { + logo: , + children: , + width: 'lg', + }, +}; \ No newline at end of file diff --git a/packages/ui-kit/src/layout/AuthShell/AuthShell.test.tsx b/packages/ui-kit/src/layout/AuthShell/AuthShell.test.tsx new file mode 100644 index 0000000..cf1e726 --- /dev/null +++ b/packages/ui-kit/src/layout/AuthShell/AuthShell.test.tsx @@ -0,0 +1,83 @@ +import { render, screen } from '@testing-library/react'; +import { AuthShell } from './AuthShell'; + +describe('AuthShell', () => { + it('renders children content', () => { + render( + +
Test Content
+
+ ); + + expect(screen.getByTestId('test-content')).toBeInTheDocument(); + expect(screen.getByText('Test Content')).toBeInTheDocument(); + }); + + it('renders logo when provided', () => { + render( + Brand Logo} + > +
Test Content
+
+ ); + + expect(screen.getByTestId('test-logo')).toBeInTheDocument(); + expect(screen.getByText('Brand Logo')).toBeInTheDocument(); + }); + + it('renders footer when provided', () => { + render( + Footer Content} + > +
Test Content
+
+ ); + + expect(screen.getByTestId('test-footer')).toBeInTheDocument(); + expect(screen.getByText('Footer Content')).toBeInTheDocument(); + }); + + it('applies width class based on width prop', () => { + const { container, rerender } = render( + +
Test Content
+
+ ); + + // Check for small width class + expect(container.querySelector('.max-w-sm')).toBeInTheDocument(); + + // Re-render with medium width + rerender( + +
Test Content
+
+ ); + + // Check for medium width class + expect(container.querySelector('.max-w-md')).toBeInTheDocument(); + + // Re-render with large width + rerender( + +
Test Content
+
+ ); + + // Check for large width class + expect(container.querySelector('.max-w-lg')).toBeInTheDocument(); + }); + + it('applies custom className when provided', () => { + const { container } = render( + +
Test Content
+
+ ); + + const authShellContainer = container.querySelector('.custom-class'); + expect(authShellContainer).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/packages/ui-kit/src/layout/AuthShell/AuthShell.tsx b/packages/ui-kit/src/layout/AuthShell/AuthShell.tsx new file mode 100644 index 0000000..90d2a5a --- /dev/null +++ b/packages/ui-kit/src/layout/AuthShell/AuthShell.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { cn } from '@/utils/cn'; +import { AuthShellProps } from './types'; + +/** + * AuthShell is a layout component for authentication-related pages like login, signup, and password reset. + * It provides a centered container with optional logo and footer slots. + */ +export const AuthShell: React.FC = ({ + logo, + children, + footer, + className, + width = 'md', +}) => { + const containerWidthClass = { + sm: 'max-w-sm', + md: 'max-w-md', + lg: 'max-w-lg', + }[width]; + + return ( +
+
+ {logo && ( +
+ {logo} +
+ )} + +
+ {children} +
+ + {footer && ( +
+ {footer} +
+ )} +
+
+ ); +}; + +AuthShell.displayName = 'AuthShell'; \ No newline at end of file diff --git a/packages/ui-kit/src/layout/AuthShell/index.ts b/packages/ui-kit/src/layout/AuthShell/index.ts new file mode 100644 index 0000000..499477f --- /dev/null +++ b/packages/ui-kit/src/layout/AuthShell/index.ts @@ -0,0 +1,2 @@ +export { AuthShell } from './AuthShell'; +export type { AuthShellProps } from './types'; \ No newline at end of file diff --git a/packages/ui-kit/src/layout/AuthShell/types.ts b/packages/ui-kit/src/layout/AuthShell/types.ts new file mode 100644 index 0000000..c812e2c --- /dev/null +++ b/packages/ui-kit/src/layout/AuthShell/types.ts @@ -0,0 +1,32 @@ +import { ReactNode } from 'react'; + +/** + * Props for the AuthShell component + */ +export interface AuthShellProps { + /** + * Logo to display at the top of the auth shell + */ + logo?: ReactNode; + + /** + * Main content of the auth shell + */ + children: ReactNode; + + /** + * Optional footer content + */ + footer?: ReactNode; + + /** + * Optional additional class name for the container + */ + className?: string; + + /** + * Width of the auth container + * @default 'md' + */ + width?: 'sm' | 'md' | 'lg'; +} \ No newline at end of file diff --git a/packages/ui-kit/src/layout/index.ts b/packages/ui-kit/src/layout/index.ts index 4692b44..bfbf985 100644 --- a/packages/ui-kit/src/layout/index.ts +++ b/packages/ui-kit/src/layout/index.ts @@ -1,2 +1,2 @@ -// Layout components will be exported here -export { } \ No newline at end of file +// Layout components +export * from './AuthShell'; \ No newline at end of file diff --git a/packages/ui-kit/tests/visual/AuthShell.spec.ts b/packages/ui-kit/tests/visual/AuthShell.spec.ts new file mode 100644 index 0000000..fea2913 --- /dev/null +++ b/packages/ui-kit/tests/visual/AuthShell.spec.ts @@ -0,0 +1,59 @@ +import { test, expect } from '@playwright/test'; + +test.describe('AuthShell Layout Visual Tests', () => { + test('Default AuthShell layout appears correctly', async ({ page }) => { + // Navigate to the Default AuthShell story + await page.goto('/iframe.html?args=&id=layout-authshell--default&viewMode=story'); + + // Wait for the content to be visible + await page.waitForSelector('.max-w-md'); + + // Take a screenshot for visual comparison + const screenshot = await page.screenshot(); + + // Compare with baseline (first run will automatically create the baseline) + expect(screenshot).toMatchSnapshot('authshell-default.png'); + }); + + test('AuthShell with Footer appears correctly', async ({ page }) => { + // Navigate to the WithFooter AuthShell story + await page.goto('/iframe.html?args=&id=layout-authshell--with-footer&viewMode=story'); + + // Wait for the content to be visible + await page.waitForSelector('.max-w-md'); + + // Take a screenshot for visual comparison + const screenshot = await page.screenshot(); + + // Compare with baseline (first run will automatically create the baseline) + expect(screenshot).toMatchSnapshot('authshell-with-footer.png'); + }); + + test('Small variant of AuthShell appears correctly', async ({ page }) => { + // Navigate to the Small AuthShell story + await page.goto('/iframe.html?args=&id=layout-authshell--small&viewMode=story'); + + // Wait for the content to be visible + await page.waitForSelector('.max-w-sm'); + + // Take a screenshot for visual comparison + const screenshot = await page.screenshot(); + + // Compare with baseline (first run will automatically create the baseline) + expect(screenshot).toMatchSnapshot('authshell-small.png'); + }); + + test('Large variant of AuthShell appears correctly', async ({ page }) => { + // Navigate to the Large AuthShell story + await page.goto('/iframe.html?args=&id=layout-authshell--large&viewMode=story'); + + // Wait for the content to be visible + await page.waitForSelector('.max-w-lg'); + + // Take a screenshot for visual comparison + const screenshot = await page.screenshot(); + + // Compare with baseline (first run will automatically create the baseline) + expect(screenshot).toMatchSnapshot('authshell-large.png'); + }); +}); \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f1106d..9ee428c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,6 +142,9 @@ importers: '@eslint/js': specifier: ^9.27.0 version: 9.27.0 + '@playwright/test': + specifier: ^1.52.0 + version: 1.52.0 '@storybook/addon-a11y': specifier: ^8.6.14 version: 8.6.14(storybook@8.6.14(prettier@3.5.3)) @@ -1399,6 +1402,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.52.0': + resolution: {integrity: sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==} + engines: {node: '>=18'} + hasBin: true + '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: @@ -2979,6 +2987,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -4069,6 +4082,16 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + playwright-core@1.52.0: + resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.52.0: + resolution: {integrity: sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==} + engines: {node: '>=18'} + hasBin: true + pngjs@6.0.0: resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} engines: {node: '>=12.13.0'} @@ -6422,6 +6445,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.52.0': + dependencies: + playwright: 1.52.0 + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.4)(react@19.1.0)': dependencies: react: 19.1.0 @@ -8342,6 +8369,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -9428,6 +9458,14 @@ snapshots: mlly: 1.7.4 pathe: 2.0.3 + playwright-core@1.52.0: {} + + playwright@1.52.0: + dependencies: + playwright-core: 1.52.0 + optionalDependencies: + fsevents: 2.3.2 + pngjs@6.0.0: {} pngjs@7.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ca0f302..0c32423 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,2 @@ packages: - - 'packages/*' \ No newline at end of file + - "packages/*" \ No newline at end of file