Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
/doc-site/schema
/test/html_reports
/test/reports
/test-results
/playwright-report
/stats.json
*.tsbuildinfo

Expand Down
114 changes: 110 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This document provides essential context about the Intercode codebase for AI ass
## Project Overview

Intercode is a convention management system built with:

- **Backend**: Ruby on Rails with GraphQL API
- **Frontend**: React with TypeScript
- **Routing**: React Router v7
Expand Down Expand Up @@ -46,6 +47,7 @@ export const action: ActionFunction<RouterContextProvider> = async ({ context, r
```

**Key points:**

- Always use `LoaderFunction<RouterContextProvider>` or `ActionFunction<RouterContextProvider>` as the type
- Get client with `context.get(apolloClientContext)`
- Import `apolloClientContext` from `'AppContexts'`
Expand All @@ -72,25 +74,29 @@ function MyComponent() {
```

**Key points:**

- Use `useApolloClient()` hook from `'@apollo/client/react'`
- Call the hook inside the component/hook function body
- Never try to use `useApolloClient()` in loaders or actions (they're not React components)

### Common Mistakes to Avoid

❌ **Don't**: Import a global client instance

```typescript
import { client } from 'useIntercodeApolloClient'; // This no longer exists
```

❌ **Don't**: Use `useApolloClient()` in loaders/actions

```typescript
export const loader: LoaderFunction = async () => {
const client = useApolloClient(); // Error: hooks can't be used here
};
```

❌ **Don't**: Try to access `client` directly in loaders without getting it from context

```typescript
export const loader: LoaderFunction = async () => {
const { data } = await client.query(...); // Error: client is not defined
Expand Down Expand Up @@ -118,6 +124,7 @@ export const loader: LoaderFunction = async () => {
### Route Structure

Routes follow a file-based convention similar to Remix/React Router v7:

- `route.tsx` or `index.tsx`: Default route component
- `$id.ts`: Dynamic route segment
- `loaders.ts`: Loader functions for the route
Expand Down Expand Up @@ -251,10 +258,7 @@ import { useAppDateTimeFormat } from './TimeUtils';

function MyComponent() {
const format = useAppDateTimeFormat();
const formatted = format(
DateTime.fromISO(isoString, { zone: timezoneName }),
'longWeekdayDateTimeWithZone'
);
const formatted = format(DateTime.fromISO(isoString, { zone: timezoneName }), 'longWeekdayDateTimeWithZone');
}
```

Expand All @@ -269,12 +273,111 @@ const formattedPrice = formatMoney(priceInCents);
## Testing Considerations

When modifying loader/action patterns:

1. Ensure loaders use `LoaderFunction<RouterContextProvider>`
2. Ensure actions use `ActionFunction<RouterContextProvider>`
3. Always get the client from context in loaders/actions
4. Run `yarn run tsc --noEmit` to check for TypeScript errors
5. Test actual navigation flows to ensure data loading works

## End-to-End Testing with Playwright

The project includes Playwright test infrastructure for browser-based end-to-end tests. The helpers are located in `playwright-tests/` and handle authentication, user creation, and permissions.

### Quick Start

```typescript
import { test, expect } from '@playwright/test';
import { setupAndLogin } from './helpers/login';

test('can access admin page', async ({ page }) => {
const conventionDomain = 'myconvention.intercode.test';

await page.goto(`https://${conventionDomain}:5050/admin`);

// Creates test user with admin permissions and logs in
await setupAndLogin(page, conventionDomain, ['update_convention']);

await expect(page.locator('h1')).toBeVisible();
});
```

### Key Helpers

**`setupAndLogin(page, conventionDomain, permissions?)`**

- Creates a test user in the database
- Grants specified permissions (default: none)
- Logs in via the UI
- Reloads the page to ensure auth state is picked up

**`ensureTestUser(conventionDomain, permissions?)`**

- Creates/updates a test user via Rails
- Grants permissions via staff positions
- Returns credentials for manual login

**`login(page, credentials)`**

- Handles the UI login flow only
- Waits for login modal, fills credentials, submits

### Permission System

Tests must explicitly request permissions. Common permissions:

- `update_convention` - Admin access to convention settings
- `read_schedule` - View schedules
- `update_events` - Manage events
- `manage_signups` - Manage user signups
- `read_reports` - View reports

See `config/permission_names.json` for all available permissions.

### Examples

```typescript
// Regular user (no special permissions)
await setupAndLogin(page, 'mycon.test');

// Admin user
await setupAndLogin(page, 'mycon.test', ['update_convention']);

// Multiple permissions
await setupAndLogin(page, 'mycon.test', ['update_events', 'read_schedule', 'manage_signups']);
```

### Environment Variables

- `TEST_EMAIL` - Email for test user (default: `playwright-test@example.com`)
- `TEST_PASSWORD` - Password (default: `TestPassword123!`)
- `RAILS_ENV` - Rails environment (default: `development`)

### Running Tests

```bash
# Run all tests
yarn playwright test

# Run specific test file
yarn playwright test my-test.spec.ts

# Run with visible browser
yarn playwright test --headed

# Debug mode
yarn playwright test --debug
```

### Best Practices

1. **Always specify convention domain** - No hardcoded defaults
2. **Request minimum permissions** - Only grant what the test needs
3. **Test users are persistent** - Created once and reused across runs
4. **Use the UI for login** - Tests use actual login flow, not session manipulation

See `playwright-tests/README.md` for comprehensive documentation.

## Build and Development

```bash
Expand All @@ -297,14 +400,17 @@ yarn test
## Common Errors and Solutions

### "Cannot find name 'client'" in loader/action

**Cause**: Trying to use a global `client` variable that doesn't exist.
**Solution**: Get client from context using `context.get(apolloClientContext)`.

### "useApolloClient is defined but never used" in file with loader

**Cause**: File has loader/action that needs context-based client, not hook-based.
**Solution**: Remove `useApolloClient` import, add `apolloClientContext` import, update loader signature.

### "Property 'instance' does not exist on type 'typeof AuthenticityTokensManager'"

**Cause**: Incorrect usage of AuthenticityTokensManager.
**Solution**: Use `AuthenticityTokensContext` with `useContext` hook instead.

Expand Down
11 changes: 0 additions & 11 deletions app/javascript/SignupRoundsAdmin/CreateNewSignupRoundForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import { ErrorDisplay, FormGroupWithLabel } from '@neinteractiveliterature/litfo
import DateTimeInput from '../BuiltInFormControls/DateTimeInput';
import MaximumEventSignupsInput from './MaximumEventSignupsInput';

import { SignupAutomationMode, SignupRoundAutomationAction } from 'graphqlTypes.generated';

type CreateNewSignupRoundFormProps = {
onCancel: () => void;
};
Expand All @@ -34,15 +32,6 @@ export default function CreateNewSignupRoundForm({ onCancel }: CreateNewSignupRo
<fetcher.Form action="/signup_rounds" method="POST">
<h6>{t('signups.signupRounds.addNewSignupRound')}</h6>
<input type="hidden" name="convention_id" value={convention?.id} />
<input
type="hidden"
name="automation_action"
value={
convention?.signup_automation_mode === SignupAutomationMode.RankedChoice
? SignupRoundAutomationAction.ExecuteRankedChoice
: ''
}
/>

<FormGroupWithLabel label={t('signups.signupRounds.start')}>
{(id) => (
Expand Down
4 changes: 2 additions & 2 deletions app/javascript/UIComponents/ScheduledValuePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@ function ScheduledValuePreviewCalendar<ValueType>({
}
if (currentWeek.length > 0) {
const weekPreview = [...currentWeek];
while (currentWeek.length < 7) {
weekPreview.push(<td key={`fill-weekday-${currentWeek.length}`} className="p-1" />);
while (weekPreview.length < 7) {
weekPreview.push(<td key={`fill-weekday-${weekPreview.length}`} className="p-1" />);
}
weekPreviews.push(<tr key={now.toISO()}>{weekPreview}</tr>);
}
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@
"@graphql-codegen/typescript-operations": "5.0.4",
"@graphql-codegen/typescript-react-apollo": "4.3.3",
"@graphql-eslint/eslint-plugin": "4.4.0",
"@playwright/test": "^1.58.1",
"@prettier/plugin-ruby": "4.0.4",
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-node-resolve": "^16.0.0",
Expand Down Expand Up @@ -209,6 +210,8 @@
"husky": "9.1.7",
"jsdom": "^27.0.0",
"lint-staged": "16.2.6",
"node-addon-api": "^8.5.0",
"node-gyp": "^12.2.0",
"prettier": "3.6.2",
"react-test-renderer": "19.2.0",
"rollup": "^4.29.2",
Expand Down
Loading
Loading