Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Run ESLint
run: npm run lint

typecheck:
name: Type Check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Run TypeScript compiler
run: npx tsc -b

test:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Run unit tests
run: npm run test -- --run

build:
name: Build
runs-on: ubuntu-latest
needs: [lint, typecheck, test]
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Build application
run: npm run build

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7

e2e:
name: E2E Tests
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Install Playwright browsers
run: npx playwright install --with-deps chromium

- name: Run E2E tests
run: npm run test:e2e -- --project=chromium

- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
84 changes: 84 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Deploy

on:
push:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: "Deployment environment"
required: true
default: "staging"
type: choice
options:
- staging
- production

jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Build application
run: npm run build

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7

deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: [build]
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'staging')
environment:
name: staging
url: ${{ steps.deploy.outputs.url }}
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/

- name: Deploy to staging
id: deploy
run: |
echo "Deploying to staging environment..."
echo "url=https://staging.example.com" >> $GITHUB_OUTPUT

deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [build]
if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production'
environment:
name: production
url: ${{ steps.deploy.outputs.url }}
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/

- name: Deploy to production
id: deploy
run: |
echo "Deploying to production environment..."
echo "url=https://production.example.com" >> $GITHUB_OUTPUT
50 changes: 50 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Security Audit

on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: "0 0 * * 1"

jobs:
audit:
name: NPM Audit
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Run npm audit
run: npm audit --audit-level=high
continue-on-error: true

- name: Run npm audit (production only)
run: npm audit --omit=dev --audit-level=moderate

dependency-review:
name: Dependency Review
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Dependency Review
uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
deny-licenses: GPL-3.0, AGPL-3.0
# Note: This step requires Dependency Graph to be enabled in repository settings.
# On forks or repos without this feature enabled, this step will fail gracefully.
continue-on-error: true
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx lint-staged
110 changes: 110 additions & 0 deletions e2e/dashboard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { test, expect } from "@playwright/test";

test.describe("Dashboard", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
});

test("should load the dashboard page", async ({ page }) => {
await expect(page).toHaveTitle(/TailAdmin/);
});

test("should display the sidebar", async ({ page }) => {
const sidebar = page.locator("aside");
await expect(sidebar).toBeVisible();
});

test("should display the header", async ({ page }) => {
const header = page.locator("header");
await expect(header).toBeVisible();
});

test("should navigate to calendar page", async ({ page }) => {
await page.click('a[href="/calendar"]');
await expect(page).toHaveURL(/calendar/);
});

test("should navigate to profile page", async ({ page }) => {
await page.click('a[href="/profile"]');
await expect(page).toHaveURL(/profile/);
});

test("should toggle dark mode", async ({ page }) => {
const themeToggle = page.locator('[data-testid="theme-toggle"]').or(
page.locator('button:has-text("Dark")').or(
page.locator('button[aria-label*="theme"]')
)
);

if (await themeToggle.count() > 0) {
await themeToggle.first().click();
await expect(page.locator("html")).toHaveClass(/dark/);
}
});
});

test.describe("Responsive Design", () => {
test("should display mobile menu on small screens", async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto("/");

const mobileMenuButton = page.locator('button[aria-label*="menu"]').or(
page.locator('button:has-text("Menu")')
);

if (await mobileMenuButton.count() > 0) {
await expect(mobileMenuButton.first()).toBeVisible();
}
});
});

test.describe("Navigation", () => {
test("should navigate to form elements page", async ({ page }) => {
await page.goto("/");
// First expand the Forms menu (it's a collapsible dropdown)
await page.getByRole("button", { name: /forms/i }).click();
// Then click the Form Elements link
await page.click('a[href="/form-elements"]');
await expect(page).toHaveURL(/form-elements/);
});

test("should navigate to basic tables page", async ({ page }) => {
await page.goto("/");
// First expand the Tables menu (it's a collapsible dropdown)
await page.getByRole("button", { name: /tables/i }).click();
// Then click the Basic Tables link
await page.click('a[href="/basic-tables"]');
await expect(page).toHaveURL(/basic-tables/);
});

test("should navigate to charts page", async ({ page }) => {
await page.goto("/");
// First expand the Charts menu (it's a collapsible dropdown)
await page.getByRole("button", { name: /charts/i }).click();
// Then click the Line Chart link
await page.click('a[href="/line-chart"]');
await expect(page).toHaveURL(/line-chart/);
});
});

test.describe("Authentication Pages", () => {
test("should display sign in page", async ({ page }) => {
await page.goto("/signin");
await expect(page.locator("form")).toBeVisible();
});

test("should display sign up page", async ({ page }) => {
await page.goto("/signup");
await expect(page.locator("form")).toBeVisible();
});
});

test.describe("404 Page", () => {
test("should display 404 page for unknown routes", async ({ page }) => {
await page.goto("/unknown-route-that-does-not-exist");
// The 404 page shows "We can't seem to find the page you are looking for!"
await expect(page.locator("body")).toContainText(
/can.?t seem to find the page|404|not found/i
);
});
});
8 changes: 4 additions & 4 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ export default tseslint.config(
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
languageOptions: {
ecmaVersion: 2022,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
Expand Down
Loading
Loading