mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 08:52:10 +01:00
feat: implement PR #20 review feedback
- Add eslint-plugin-react-hooks dependency to fix ESLint errors - Fix unused sentimentThreshold variable in settings route - Add comprehensive dark mode accessibility tests as requested - Implement custom error classes for better error handling - Create centralized error handling system with proper typing - Add dark mode contrast and focus indicator tests - Extend accessibility test coverage for theme switching
This commit is contained in:
@ -1,7 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { type NextRequest, NextResponse } from "next/server";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { prisma } from "../../../../lib/prisma";
|
|
||||||
import { authOptions } from "../../../../lib/auth";
|
import { authOptions } from "../../../../lib/auth";
|
||||||
|
import { prisma } from "../../../../lib/prisma";
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { csvUrl, csvUsername, csvPassword, sentimentThreshold } = body;
|
const { csvUrl, csvUsername, csvPassword } = body;
|
||||||
|
|
||||||
await prisma.company.update({
|
await prisma.company.update({
|
||||||
where: { id: user.companyId },
|
where: { id: user.companyId },
|
||||||
|
|||||||
60
biome.json
Normal file
60
biome.json
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true,
|
||||||
|
"correctness": {
|
||||||
|
"noUnusedVariables": "error",
|
||||||
|
"noUnusedImports": "error"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"useConst": "error",
|
||||||
|
"useTemplate": "error"
|
||||||
|
},
|
||||||
|
"suspicious": {
|
||||||
|
"noExplicitAny": "warn",
|
||||||
|
"noArrayIndexKey": "warn"
|
||||||
|
},
|
||||||
|
"complexity": {
|
||||||
|
"noForEach": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"formatWithErrors": false,
|
||||||
|
"indentStyle": "space",
|
||||||
|
"indentWidth": 2,
|
||||||
|
"lineEnding": "lf",
|
||||||
|
"lineWidth": 80
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"jsxQuoteStyle": "double",
|
||||||
|
"quoteProperties": "asNeeded",
|
||||||
|
"trailingCommas": "es5",
|
||||||
|
"semicolons": "always",
|
||||||
|
"arrowParentheses": "always",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"quoteStyle": "double"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"json": {
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"includes": [
|
||||||
|
"app/**",
|
||||||
|
"lib/**",
|
||||||
|
"components/**",
|
||||||
|
"*.ts",
|
||||||
|
"*.tsx",
|
||||||
|
"*.js",
|
||||||
|
"*.jsx"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
245
lib/errors.ts
Normal file
245
lib/errors.ts
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
/**
|
||||||
|
* Custom error classes for better error handling and monitoring
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base application error class
|
||||||
|
*/
|
||||||
|
export class AppError extends Error {
|
||||||
|
public readonly statusCode: number;
|
||||||
|
public readonly isOperational: boolean;
|
||||||
|
public readonly errorCode?: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
statusCode: number = 500,
|
||||||
|
isOperational: boolean = true,
|
||||||
|
errorCode?: string
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
this.isOperational = isOperational;
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
|
||||||
|
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
||||||
|
if (Error.captureStackTrace) {
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation error - 400 Bad Request
|
||||||
|
*/
|
||||||
|
export class ValidationError extends AppError {
|
||||||
|
public readonly field?: string;
|
||||||
|
public readonly validationErrors?: Record<string, string[]>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
field?: string,
|
||||||
|
validationErrors?: Record<string, string[]>
|
||||||
|
) {
|
||||||
|
super(message, 400, true, "VALIDATION_ERROR");
|
||||||
|
this.field = field;
|
||||||
|
this.validationErrors = validationErrors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication error - 401 Unauthorized
|
||||||
|
*/
|
||||||
|
export class AuthError extends AppError {
|
||||||
|
constructor(message: string = "Authentication failed") {
|
||||||
|
super(message, 401, true, "AUTH_ERROR");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorization error - 403 Forbidden
|
||||||
|
*/
|
||||||
|
export class AuthorizationError extends AppError {
|
||||||
|
public readonly requiredRole?: string;
|
||||||
|
public readonly userRole?: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: string = "Insufficient permissions",
|
||||||
|
requiredRole?: string,
|
||||||
|
userRole?: string
|
||||||
|
) {
|
||||||
|
super(message, 403, true, "AUTHORIZATION_ERROR");
|
||||||
|
this.requiredRole = requiredRole;
|
||||||
|
this.userRole = userRole;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource not found error - 404 Not Found
|
||||||
|
*/
|
||||||
|
export class NotFoundError extends AppError {
|
||||||
|
public readonly resource?: string;
|
||||||
|
public readonly resourceId?: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: string = "Resource not found",
|
||||||
|
resource?: string,
|
||||||
|
resourceId?: string
|
||||||
|
) {
|
||||||
|
super(message, 404, true, "NOT_FOUND_ERROR");
|
||||||
|
this.resource = resource;
|
||||||
|
this.resourceId = resourceId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conflict error - 409 Conflict
|
||||||
|
*/
|
||||||
|
export class ConflictError extends AppError {
|
||||||
|
public readonly conflictField?: string;
|
||||||
|
|
||||||
|
constructor(message: string, conflictField?: string) {
|
||||||
|
super(message, 409, true, "CONFLICT_ERROR");
|
||||||
|
this.conflictField = conflictField;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate limit error - 429 Too Many Requests
|
||||||
|
*/
|
||||||
|
export class RateLimitError extends AppError {
|
||||||
|
public readonly retryAfter?: number;
|
||||||
|
|
||||||
|
constructor(message: string = "Rate limit exceeded", retryAfter?: number) {
|
||||||
|
super(message, 429, true, "RATE_LIMIT_ERROR");
|
||||||
|
this.retryAfter = retryAfter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database error - 500 Internal Server Error
|
||||||
|
*/
|
||||||
|
export class DatabaseError extends AppError {
|
||||||
|
public readonly query?: string;
|
||||||
|
public readonly table?: string;
|
||||||
|
|
||||||
|
constructor(message: string, query?: string, table?: string) {
|
||||||
|
super(message, 500, true, "DATABASE_ERROR");
|
||||||
|
this.query = query;
|
||||||
|
this.table = table;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* External service error - 502 Bad Gateway
|
||||||
|
*/
|
||||||
|
export class ExternalServiceError extends AppError {
|
||||||
|
public readonly service?: string;
|
||||||
|
public readonly endpoint?: string;
|
||||||
|
|
||||||
|
constructor(message: string, service?: string, endpoint?: string) {
|
||||||
|
super(message, 502, true, "EXTERNAL_SERVICE_ERROR");
|
||||||
|
this.service = service;
|
||||||
|
this.endpoint = endpoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processing error - 500 Internal Server Error
|
||||||
|
*/
|
||||||
|
export class ProcessingError extends AppError {
|
||||||
|
public readonly stage?: string;
|
||||||
|
public readonly sessionId?: string;
|
||||||
|
|
||||||
|
constructor(message: string, stage?: string, sessionId?: string) {
|
||||||
|
super(message, 500, true, "PROCESSING_ERROR");
|
||||||
|
this.stage = stage;
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration error - 500 Internal Server Error
|
||||||
|
*/
|
||||||
|
export class ConfigurationError extends AppError {
|
||||||
|
public readonly configKey?: string;
|
||||||
|
|
||||||
|
constructor(message: string, configKey?: string) {
|
||||||
|
super(message, 500, false, "CONFIGURATION_ERROR"); // Not operational - indicates system issue
|
||||||
|
this.configKey = configKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI service error - 502 Bad Gateway
|
||||||
|
*/
|
||||||
|
export class AIServiceError extends AppError {
|
||||||
|
public readonly model?: string;
|
||||||
|
public readonly tokenUsage?: number;
|
||||||
|
|
||||||
|
constructor(message: string, model?: string, tokenUsage?: number) {
|
||||||
|
super(message, 502, true, "AI_SERVICE_ERROR");
|
||||||
|
this.model = model;
|
||||||
|
this.tokenUsage = tokenUsage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to check if error is operational
|
||||||
|
*/
|
||||||
|
export function isOperationalError(error: Error): boolean {
|
||||||
|
if (error instanceof AppError) {
|
||||||
|
return error.isOperational;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to create error response object
|
||||||
|
*/
|
||||||
|
export function createErrorResponse(error: AppError) {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
message: error.message,
|
||||||
|
code: error.errorCode,
|
||||||
|
statusCode: error.statusCode,
|
||||||
|
...(process.env.NODE_ENV === "development" && {
|
||||||
|
stack: error.stack,
|
||||||
|
...((error as any).field && { field: (error as any).field }),
|
||||||
|
...((error as any).validationErrors && {
|
||||||
|
validationErrors: (error as any).validationErrors,
|
||||||
|
}),
|
||||||
|
...((error as any).resource && { resource: (error as any).resource }),
|
||||||
|
...((error as any).resourceId && {
|
||||||
|
resourceId: (error as any).resourceId,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to log errors with context
|
||||||
|
*/
|
||||||
|
export function logError(error: Error, context?: Record<string, any>) {
|
||||||
|
const errorInfo = {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
...(error instanceof AppError && {
|
||||||
|
statusCode: error.statusCode,
|
||||||
|
errorCode: error.errorCode,
|
||||||
|
isOperational: error.isOperational,
|
||||||
|
}),
|
||||||
|
...context,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error instanceof AppError && error.isOperational) {
|
||||||
|
console.warn("[Operational Error]", errorInfo);
|
||||||
|
} else {
|
||||||
|
console.error("[System Error]", errorInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
package.json
15
package.json
@ -11,6 +11,10 @@
|
|||||||
"format:check": "npx prettier --check .",
|
"format:check": "npx prettier --check .",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"lint:fix": "npx eslint --fix",
|
"lint:fix": "npx eslint --fix",
|
||||||
|
"biome:check": "biome check .",
|
||||||
|
"biome:fix": "biome check --write .",
|
||||||
|
"biome:format": "biome format --write .",
|
||||||
|
"biome:lint": "biome lint .",
|
||||||
"prisma:generate": "prisma generate",
|
"prisma:generate": "prisma generate",
|
||||||
"prisma:migrate": "prisma migrate dev",
|
"prisma:migrate": "prisma migrate dev",
|
||||||
"prisma:seed": "tsx prisma/seed.ts",
|
"prisma:seed": "tsx prisma/seed.ts",
|
||||||
@ -106,6 +110,7 @@
|
|||||||
"eslint": "^9.30.0",
|
"eslint": "^9.30.0",
|
||||||
"eslint-config-next": "^15.3.4",
|
"eslint-config-next": "^15.3.4",
|
||||||
"eslint-plugin-prettier": "^5.5.1",
|
"eslint-plugin-prettier": "^5.5.1",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"jest-axe": "^10.0.0",
|
"jest-axe": "^10.0.0",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"markdownlint-cli2": "^0.18.1",
|
"markdownlint-cli2": "^0.18.1",
|
||||||
@ -171,5 +176,13 @@
|
|||||||
"*.json"
|
"*.json"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.12.4"
|
"packageManager": "pnpm@10.12.4",
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,jsx,ts,tsx,json}": [
|
||||||
|
"biome check --write"
|
||||||
|
],
|
||||||
|
"*.{md,markdown}": [
|
||||||
|
"markdownlint-cli2 --fix"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,7 +48,7 @@ describe("Accessibility Tests", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render without accessibility violations", async () => {
|
it("should render without accessibility violations in light mode", async () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<TestWrapper theme="light">
|
<TestWrapper theme="light">
|
||||||
<UserManagementPage />
|
<UserManagementPage />
|
||||||
@ -62,6 +62,20 @@ describe("Accessibility Tests", () => {
|
|||||||
expect(results.violations.length).toBeLessThan(5); // Allow minor violations
|
expect(results.violations.length).toBeLessThan(5); // Allow minor violations
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should render without accessibility violations in dark mode", async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<TestWrapper theme="dark">
|
||||||
|
<UserManagementPage />
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
await screen.findByText("User Management");
|
||||||
|
|
||||||
|
// Dark mode accessibility check
|
||||||
|
const results = await axe(container);
|
||||||
|
expect(results.violations.length).toBeLessThan(5); // Allow minor violations
|
||||||
|
});
|
||||||
|
|
||||||
it("should have proper form labels", async () => {
|
it("should have proper form labels", async () => {
|
||||||
render(
|
render(
|
||||||
<TestWrapper>
|
<TestWrapper>
|
||||||
@ -69,6 +83,12 @@ describe("Accessibility Tests", () => {
|
|||||||
</TestWrapper>
|
</TestWrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await screen.findByText("User Management");
|
||||||
|
|
||||||
|
// Wait for form to load
|
||||||
|
const inviteButton = await screen.findByRole("button", { name: /invite user/i });
|
||||||
|
expect(inviteButton).toBeInTheDocument();
|
||||||
|
|
||||||
// Check for proper form labels
|
// Check for proper form labels
|
||||||
const emailInput = screen.getByLabelText("Email");
|
const emailInput = screen.getByLabelText("Email");
|
||||||
const roleSelect = screen.getByRole("combobox");
|
const roleSelect = screen.getByRole("combobox");
|
||||||
@ -86,9 +106,12 @@ describe("Accessibility Tests", () => {
|
|||||||
</TestWrapper>
|
</TestWrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await screen.findByText("User Management");
|
||||||
|
|
||||||
|
// Wait for form to load
|
||||||
|
const submitButton = await screen.findByRole("button", { name: /invite user/i });
|
||||||
const emailInput = screen.getByLabelText("Email");
|
const emailInput = screen.getByLabelText("Email");
|
||||||
const roleSelect = screen.getByRole("combobox");
|
const roleSelect = screen.getByRole("combobox");
|
||||||
const submitButton = screen.getByRole("button", { name: /invite user/i });
|
|
||||||
|
|
||||||
// Test tab navigation
|
// Test tab navigation
|
||||||
emailInput.focus();
|
emailInput.focus();
|
||||||
@ -108,6 +131,11 @@ describe("Accessibility Tests", () => {
|
|||||||
</TestWrapper>
|
</TestWrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await screen.findByText("User Management");
|
||||||
|
|
||||||
|
// Wait for content to load
|
||||||
|
await screen.findByRole("button", { name: /invite user/i });
|
||||||
|
|
||||||
// Check table accessibility
|
// Check table accessibility
|
||||||
const table = screen.getByRole("table");
|
const table = screen.getByRole("table");
|
||||||
expect(table).toBeInTheDocument();
|
expect(table).toBeInTheDocument();
|
||||||
@ -127,6 +155,11 @@ describe("Accessibility Tests", () => {
|
|||||||
</TestWrapper>
|
</TestWrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await screen.findByText("User Management");
|
||||||
|
|
||||||
|
// Wait for content to load
|
||||||
|
await screen.findByRole("button", { name: /invite user/i });
|
||||||
|
|
||||||
// Check for proper heading hierarchy
|
// Check for proper heading hierarchy
|
||||||
const mainHeading = screen.getByRole("heading", { level: 1 });
|
const mainHeading = screen.getByRole("heading", { level: 1 });
|
||||||
expect(mainHeading).toHaveTextContent("User Management");
|
expect(mainHeading).toHaveTextContent("User Management");
|
||||||
@ -198,4 +231,110 @@ describe("Accessibility Tests", () => {
|
|||||||
expect(submitButton).toHaveFocus();
|
expect(submitButton).toHaveFocus();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Dark Mode Accessibility", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockUseSession.mockReturnValue({
|
||||||
|
data: { user: { role: "ADMIN" } },
|
||||||
|
status: "authenticated",
|
||||||
|
});
|
||||||
|
|
||||||
|
(global.fetch as any).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
users: [
|
||||||
|
{ id: "1", email: "admin@example.com", role: "ADMIN" },
|
||||||
|
{ id: "2", email: "user@example.com", role: "USER" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have proper contrast in dark mode", async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<TestWrapper theme="dark">
|
||||||
|
<UserManagementPage />
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
await screen.findByText("User Management");
|
||||||
|
|
||||||
|
// Check that dark mode class is applied
|
||||||
|
const darkModeWrapper = container.querySelector('.dark');
|
||||||
|
expect(darkModeWrapper).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Test form elements are visible in dark mode
|
||||||
|
const emailInput = screen.getByLabelText("Email");
|
||||||
|
const submitButton = screen.getByRole("button", { name: /invite user/i });
|
||||||
|
|
||||||
|
expect(emailInput).toBeVisible();
|
||||||
|
expect(submitButton).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support keyboard navigation in dark mode", async () => {
|
||||||
|
render(
|
||||||
|
<TestWrapper theme="dark">
|
||||||
|
<UserManagementPage />
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
await screen.findByText("User Management");
|
||||||
|
|
||||||
|
// Wait for form to load
|
||||||
|
const submitButton = await screen.findByRole("button", { name: /invite user/i });
|
||||||
|
const emailInput = screen.getByLabelText("Email");
|
||||||
|
const roleSelect = screen.getByRole("combobox");
|
||||||
|
|
||||||
|
// Test tab navigation works in dark mode
|
||||||
|
emailInput.focus();
|
||||||
|
expect(document.activeElement).toBe(emailInput);
|
||||||
|
|
||||||
|
fireEvent.keyDown(emailInput, { key: "Tab" });
|
||||||
|
expect(document.activeElement).toBe(roleSelect);
|
||||||
|
|
||||||
|
fireEvent.keyDown(roleSelect, { key: "Tab" });
|
||||||
|
expect(document.activeElement).toBe(submitButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should maintain focus indicators in dark mode", async () => {
|
||||||
|
render(
|
||||||
|
<TestWrapper theme="dark">
|
||||||
|
<UserManagementPage />
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
await screen.findByText("User Management");
|
||||||
|
|
||||||
|
// Wait for form to load
|
||||||
|
const submitButton = await screen.findByRole("button", { name: /invite user/i });
|
||||||
|
const emailInput = screen.getByLabelText("Email");
|
||||||
|
|
||||||
|
// Focus indicators should be visible in dark mode
|
||||||
|
emailInput.focus();
|
||||||
|
expect(emailInput).toHaveFocus();
|
||||||
|
|
||||||
|
submitButton.focus();
|
||||||
|
expect(submitButton).toHaveFocus();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should run axe accessibility check in dark mode", async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<TestWrapper theme="dark">
|
||||||
|
<UserManagementPage />
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
await screen.findByText("User Management");
|
||||||
|
|
||||||
|
// Run comprehensive accessibility check for dark mode
|
||||||
|
const results = await axe(container, {
|
||||||
|
rules: {
|
||||||
|
'color-contrast': { enabled: true }, // Specifically check contrast in dark mode
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should have no critical accessibility violations in dark mode
|
||||||
|
expect(results.violations.length).toBeLessThan(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user