Skip to content
Merged
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
9 changes: 8 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -325,16 +325,23 @@ jobs:
path: vitest-coverage
- name: Merge coverage reports
run: npx cobertura-merge -o merged-coverage.xml package1=minitest-coverage/coverage.xml package2=vitest-coverage/cobertura-coverage.xml package3=minitest-system-coverage/coverage.xml
- name: Archive merged coverage report
uses: actions/upload-artifact@v5
if: always()
with:
name: coverage-main
path: merged-coverage.xml
- name: Generate Coverage Report
uses: clearlyip/code-coverage-report-action@v6
id: code_coverage_report_action
if: ${{ github.actor != 'dependabot[bot]'}}
with:
filename: "merged-coverage.xml"
fail_on_negative_difference: true
artifact_download_workflow_names: "ci,cron"
only_list_changed_files: true
- name: Add Coverage PR Comment
if: github.event_name == 'pull_request'
if: steps.code_coverage_report_action.outputs.file != '' && github.event_name == 'pull_request' && (success() || failure())
uses: marocchino/sticky-pull-request-comment@v2
with:
recreate: true
Expand Down
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