Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0080b3b
feat: add EDIT_PROFILE_PAGE constant
river0525 Mar 27, 2026
e9ea7a0
feat: re-enable AtCoder verification tab on /users/edit
river0525 Mar 27, 2026
833d928
feat: require AtCoder verification to cast a grade vote
river0525 Mar 27, 2026
0ad162f
feat: show AtCoder verification prompt on vote detail page for unveri…
river0525 Mar 27, 2026
1c5681e
feat: propagate isAtCoderVerified to VotableGrade dropdown on problem…
river0525 Mar 27, 2026
a8ca83c
feat: open AtCoder tab when navigating from unverified-user prompt vi…
river0525 Mar 27, 2026
e5db921
docs: add conflict resolution plan for PR #3319 × staging
KATO-Hiro Mar 29, 2026
3302158
fix: resolve staging merge conflicts for PR #3319 atcoder-verified-vo…
KATO-Hiro Mar 29, 2026
a7b16c2
fix: remove username from server-side error log in users/edit load
KATO-Hiro Mar 29, 2026
3c67710
docs(rules): add load() model grouping and component prop patterns
KATO-Hiro Mar 29, 2026
3bdbfed
chore: Fix format
KATO-Hiro Mar 29, 2026
4d391c1
chore: Fix format
KATO-Hiro Mar 29, 2026
2b1d983
chore: Fix format
KATO-Hiro Mar 29, 2026
9645d5b
chore: Fix https status
KATO-Hiro Mar 29, 2026
d730ffb
docs: update conflict resolution plan checklist and add review findings
KATO-Hiro Mar 29, 2026
af142c0
docs: add CodeRabbit findings and DRY refactor task to plan
KATO-Hiro Mar 29, 2026
27566f6
refactor: extract parseUsernameAndAuthorize helper in users/edit actions
KATO-Hiro Mar 29, 2026
16d05e5
docs: mark DRY refactor complete and triage CodeRabbit findings in plan
KATO-Hiro Mar 29, 2026
e4365ac
docs: trim plan to decision-relevant content only
KATO-Hiro Mar 29, 2026
eef8613
docs: Remove old plan
KATO-Hiro Mar 29, 2026
78599b5
tests: Fix broken e2e tests
KATO-Hiro Mar 29, 2026
25f0614
docs: Add and update rules
KATO-Hiro Mar 29, 2026
791969a
chore: Remove old plan
KATO-Hiro Mar 29, 2026
b8fd4ff
docs: update .env.example comment for AtCoder verification API URL
river0525 Mar 29, 2026
31c501a
fix: address CodeRabbit round 1 findings
river0525 Mar 29, 2026
83d58c3
fix: address CodeRabbit round 2 findings
river0525 Mar 29, 2026
caedfa9
fix: address CodeRabbit round 3 findings
river0525 Mar 29, 2026
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
43 changes: 43 additions & 0 deletions .claude/rules/svelte-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,46 @@ const map: Partial<Record<WorkBookType, Component<Props>>> = {
};
// Safe to guard with: {#if map[type]} or if (map[type])
```

## Props — Pass Domain Model Objects; Derive Computed Values Internally

When a component's data comes from a domain model, pass the model object as a single prop
rather than individual fields. This keeps the call site in sync with the model automatically.

Computed values (status, labels, flags derived from multiple fields) must NOT be props —
they belong inside the component as `$derived`.

```typescript
// Bad: individual fields + derived value as prop
interface Props {
handle: string;
validationCode: string;
isValidated: boolean;
status: string; // derived — should not be a prop
}

// Good: model object as prop; status derived inside
// (username is from User model; atCoderAccount is from a separate domain model — two props is correct here)
interface Props {
username: string;
atCoderAccount: { handle: string; validationCode: string; isValidated: boolean };
}
let { username, atCoderAccount }: Props = $props();
const status = $derived(atCoderAccount.isValidated ? 'validated' : ...);
```

Call site passes the object directly from `$derived(data.atCoderAccount)`.

## $derived for data.\* Fields in +page.svelte

When reading fields from `data` in a `+page.svelte`, use `$derived` rather than plain assignment:

```typescript
// Bad: stale after load() re-runs following a form action
const atCoderAccount = data.atCoderAccount;

// Good: stays in sync when SvelteKit re-runs load() after an action
const atCoderAccount = $derived(data.atCoderAccount);
```

`data` is a reactive prop that SvelteKit updates after each form action. A plain assignment captures the initial value only.
22 changes: 22 additions & 0 deletions .claude/rules/sveltekit.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,25 @@ The same pattern applies to `url.searchParams.get()` in `+server.ts` handlers.
## Page Component Props

SvelteKit page components (`+page.svelte`) accept only `data` and `form` as props (`svelte/valid-prop-names-in-kit-pages`). Commented-out features that reference other props are not "dead code" — remove only the violating prop declaration, preserve the feature code.

## load() — Group Related Model Fields as Objects

When a `load()` function returns fields from the same domain model (e.g., `AtCoderAccount`),
group them as an object rather than flattening to top-level keys.
Apply default values at this boundary so the page component typically does not need to handle `undefined`.

```typescript
// Bad: flat, scattered across top-level keys
atcoder_username: user?.atCoderAccount?.handle ?? '',
atcoder_validationcode: user?.atCoderAccount?.validationCode ?? '',
is_validated: user?.atCoderAccount?.isValidated ?? false,

// Good: grouped by model, defaults absorbed here
atCoderAccount: {
handle: user?.atCoderAccount?.handle ?? '',
validationCode: user?.atCoderAccount?.validationCode ?? '',
isValidated: user?.atCoderAccount?.isValidated ?? false,
},
```

When consuming in `+page.svelte`, use `$derived` to maintain reactivity across load() re-runs after form actions (see svelte-components.md).
122 changes: 122 additions & 0 deletions .claude/rules/testing-e2e.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
description: E2E testing rules and patterns (Playwright)
paths:
- '**/*.spec.ts'
- 'e2e/**'
---

# E2E Tests (Playwright)

## No Path Aliases

The `e2e/` directory is outside SvelteKit's build pipeline — `$lib`, `$features`, and other path aliases are not resolved. Define string constant values as local constants with a reference comment:

```typescript
// Mirrors WorkBookTab.SOLUTION from $features/workbooks/types/workbook
const TAB_SOLUTION = 'solution';
```

Avoid importing values from `src/` in E2E test files. Type-only imports (`import type`) are acceptable since they are erased at compile time:

```typescript
// Bad: runtime import — path alias not resolved in e2e/
import { TAB_SOLUTION } from '$features/workbooks/types/workbook';

// Good: type-only import — compile-time only
import type { WorkBookTab } from '$features/workbooks/types/workbook';
const TAB_SOLUTION: WorkBookTab = 'solution';
```

## Describe Hierarchy

When a `describe` block for a user role grows large, split it by behavioral dimension rather than adding more flat `test()` calls:

```typescript
test.describe('logged-in user', () => {
test.describe('tab visibility', () => { ... });
test.describe('URL parameter handling', () => { ... });
test.describe('navigation interactions', () => { ... });
test.describe('session state', () => { ... });
});
```

## Parameterized Tests

Playwright has no native `test.each`. Use `for...of` loops — the official recommended pattern.

**Single test with a loop** — use when testing a sequence or workflow within one test:

```typescript
// Mirrors TaskGrade from $lib/types/task — do not import from src/ in E2E files
const GRADES = ['Q10', 'Q9', 'Q8'] as const;

for (const grade of GRADES) {
await gradeButton(page, grade).click();
await expect(page).toHaveURL(`?grades=${grade}`);
}
```

**Multiple tests from parameters** — use when each parameter represents an independent case:

```typescript
const GRADES = ['Q10', 'Q9', 'Q8'] as const;

for (const grade of GRADES) {
test(`filters by grade ${grade}`, async ({ page }) => {
await page.goto('/tasks');
await gradeButton(page, grade).click();
await expect(page).toHaveURL(`?grades=${grade}`);
});
}
```

## Assertions

After an interaction that changes element state (active tab, toggle, selection), assert the _new_ state — not just that the element is visible, which may have been true before the interaction. Assert an active CSS class, `aria-selected`, or similar attribute instead of `toBeVisible()`.

## Flowbite Toggle

Flowbite's `Toggle` renders an `sr-only` `<input type="checkbox">` inside a `<label>`. Clicking the input directly fails because the visual `<span>` sibling intercepts pointer events. Click the label wrapper instead:

```typescript
const toggleInput = page.locator('input[aria-label="<aria-label value>"]');
const toggleLabel = page.locator('label:has(input[aria-label="<aria-label value>"])');

await toggleLabel.click();
await expect(toggleInput).toBeChecked({ checked: true });
```

The same pattern applies to any Flowbite component that visually overlays its native input (e.g. `Checkbox`, `Radio`).

## Strict Mode: Scope Locators to the Content Area

When the navbar and page body both contain a link or button with the same text (e.g., a breadcrumb and a nav link share the same label), `getByRole` in strict mode will find multiple matches and throw. Scope the locator to the page's content container:

```typescript
// Bad: matches navbar link AND breadcrumb link
await page.getByRole('link', { name: 'グレード投票' }).click();

// Good: scoped to page content only
await page.locator('.container').locator('nav').getByRole('link', { name: 'グレード投票' }).click();
await page.locator('.container').getByRole('link', { name: 'ログイン' }).click();
```

Use `.container` (page content wrapper) to exclude the global navbar. Prefer the narrowest scope that remains stable — breadcrumb `nav` inside `.container` is more precise than `.container` alone when the link only appears there.

## Conditional Skip Based on Runtime State

When a test depends on DB or session state that may vary across environments (e.g., a user's AtCoder verification status), use `test.skip(condition, reason)` inside the test body instead of a static `test.skip`. This way the test runs automatically when the precondition is met:

```typescript
test('sees vote grade buttons', async ({ page }) => {
await page.goto(url);

const isUnverified = await page.getByText('AtCoderアカウントの認証が必要です').isVisible();
test.skip(isUnverified, 'test user is not AtCoder-verified');

// assertions below run only when precondition holds
await expect(page.locator('form[action="?/voteAbsoluteGrade"]')).toBeVisible();
});
```

Prefer this over a hard-coded `test.skip` whenever the condition is observable on the page.
125 changes: 40 additions & 85 deletions .claude/rules/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,6 @@ E2E test files must use the `.spec.ts` extension. `playwright.config.ts` matches
- Use `toBe(true)` / `toBe(false)` over `toBeTruthy()` / `toBeFalsy()`
- For DB query tests, assert `orderBy`, `include`, and other significant parameters with `expect.objectContaining` — not just `where`. When a returned field (e.g. `authorName`) depends on an `include` relation, that `include` clause must be part of the assertion, or a regression in the query shape will go undetected
- Enum membership: `in` traverses the prototype chain; use `Object.hasOwn(Enum, value)` instead
- **E2E state transitions**: after an interaction that changes element state (active tab, toggle, selection), assert the _new_ state — not just that the element is visible, which may have been true before the interaction. Assert an active CSS class, `aria-selected`, or similar attribute instead of `toBeVisible()`

## Cleanup in Tests

Wrap DB-mutating cleanup in `try/finally` — a failing assertion skips cleanup and contaminates later tests:

```typescript
try {
await doSomething();
expect(result).toBe(expected);
} finally {
await restoreState();
}
```

## Test Data

Expand All @@ -79,9 +65,22 @@ try {
- After `.filter()` on fixtures, verify actual contents — same ID may refer to a different entity after fixture updates
- **Description ↔ code path alignment**: when a test name describes a specific scenario (e.g. "tie-break"), verify the fixture actually exercises that code path. A test that passes without reaching the branch it claims to cover gives false confidence

## Mock Helpers
## Coverage

- Run `pnpm coverage` for coverage report
- Target: 80% lines, 80% branches

## Test Order Mirrors Source Order

Order `describe` blocks in service and utils test files to match the declaration order of functions in the source file. Misalignment makes it harder to cross-reference tests and implementation.

## Service Layer Unit Tests

Extract repeated mock patterns into helpers in the test file. For Prisma service tests, define the return type alias once and use it across all helpers:
Service tests mock Prisma via `vi.mock('$lib/server/database', ...)` — no real DB mutations occur.

### Mock Helpers

Extract repeated mock patterns into helpers in the test file. Define the return type alias once and use it across all helpers:

```typescript
type PrismaWorkBook = Awaited<ReturnType<typeof prisma.workBook.findUnique>>;
Expand All @@ -104,27 +103,24 @@ function mockCount(value: number) {

Extract `mockFindUnique`, `mockFindMany`, and `mockCount` as the standard trio for service tests that touch a single Prisma model. Add `mockCreate`, `mockTransaction`, and `mockDelete` when those operations are also tested.

## Component Vitest Unit Tests
### Cleanup for Integration Tests and Tests with Real Side Effects

Omit Vitest unit tests for a Svelte component when **both** conditions hold:
This does not apply to standard service layer unit tests that use Prisma mocks.

1. The component is template-only (no logic beyond prop bindings and basic conditionals)
2. The component is covered by E2E tests

When a component contains extracted logic (e.g. derived values, event handlers, utility calls), add unit tests for that logic in the nearest `utils/` file instead of testing the component directly.

## Testing Extracted Utilities

- Add tests at extraction time, not later
- For URL manipulation: assert the original URL is not mutated
- For multi-column operations (e.g., DnD): assert both source and destination columns
If a test performs real DB mutations, file system changes, external API calls, or other stateful side effects that persist beyond the test (e.g., integration tests, seed scripts), wrap assertions in `try/finally` — a failing assertion skips cleanup and contaminates later tests:

## Coverage
```typescript
try {
await doSomething();
expect(result).toBe(expected);
} finally {
await restoreState();
}
```

- Run `pnpm coverage` for coverage report
- Target: 80% lines, 70% branches
This is not needed for standard service unit tests that use Prisma mocks.

## Service Layer Split for Testability
### File Split for Testability

When a service file mixes DB operations and pure functions, split it into two files:

Expand All @@ -133,64 +129,23 @@ When a service file mixes DB operations and pure functions, split it into two fi

Stop the split if internal helpers (e.g. `fetchUnplacedWorkbooks`) would be fragmented across files — cohesion matters more than the split itself.

## HTTP Mocking

Use Nock for external HTTP calls. See `src/test/lib/clients/` for examples.

## Test Order Mirrors Source Order

Order `describe` blocks in service and utils test files to match the declaration order of functions in the source file. Misalignment makes it harder to cross-reference tests and implementation.

## E2E Tests

### No Path Aliases

The `e2e/` directory is outside SvelteKit's build pipeline — `$lib`, `$features`, and other path aliases are not resolved. Define URL string values as local constants with a reference comment:

```typescript
// Mirrors WorkBookTab.SOLUTION from $features/workbooks/types/workbook
const TAB_SOLUTION = 'solution';
```

Avoid importing types from `src/` in E2E test files.

### Describe Hierarchy

When a `describe` block for a user role grows large, split it by behavioral dimension rather than adding more flat `test()` calls:

```typescript
test.describe('logged-in user', () => {
test.describe('tab visibility', () => { ... });
test.describe('URL parameter handling', () => { ... });
test.describe('navigation interactions', () => { ... });
test.describe('session state', () => { ... });
});
```

### Parameterized Tests
## Component Vitest Unit Tests

Playwright has no native `test.each`. Use `for...of` loops — the official recommended pattern:
E2E tests are complementary to, not a substitute for, unit tests. Add Vitest unit tests for any component logic (derived values, event handlers, utility calls) by extracting it to the nearest `utils/` file and testing there.

```typescript
// Mirrors TaskGrade from $lib/types/task — do not import from src/ in E2E files
const GRADES = ['Q10', 'Q9', 'Q8'] as const;
You may omit a component-level Vitest test when **both** conditions hold:

for (const grade of GRADES) {
await gradeButton(grade).click();
await expect(page).toHaveURL(`?grades=${grade}`);
}
```
1. The component is template-only (no logic beyond prop bindings and simple `{#if}`/`{#each}` blocks that only render — no inline function calls, ternaries with side effects, derived computations, or nested logic)
2. The component's rendering paths are covered by E2E tests

### Flowbite Toggle
When a component contains extracted logic (e.g. derived values, event handlers, utility calls), add unit tests for that logic in the nearest `utils/` file instead of testing the component directly.

Flowbite's `Toggle` renders an `sr-only` `<input type="checkbox">` inside a `<label>`. Clicking the input directly fails because the visual `<span>` sibling intercepts pointer events. Click the label wrapper instead:
## Testing Extracted Utilities

```typescript
const toggleInput = page.locator('input[aria-label="<aria-label value>"]');
const toggleLabel = page.locator('label:has(input[aria-label="<aria-label value>"])');
- Add tests at extraction time, not later
- For URL manipulation: assert the original URL is not mutated
- For multi-column operations (e.g., DnD): assert both source and destination columns

await toggleLabel.click();
await expect(toggleInput).toBeChecked({ checked: true });
```
## HTTP Mocking

The same pattern applies to any Flowbite component that visually overlays its native input (e.g. `Checkbox`, `Radio`).
Use Nock for external HTTP calls. See `src/test/lib/clients/` for examples.
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# AtCoder affiliation confirmation API endpoint
# Set this to the actual endpoint URL in your .env file (do not commit real URLs here)
# AtCoder affiliation confirmation API endpoint (NoviSteps organization crawler)
# See team documentation for the actual URL.
Comment thread
KATO-Hiro marked this conversation as resolved.
CONFIRM_API_URL=https://your-confirm-api-endpoint.example.com/confirm
4 changes: 2 additions & 2 deletions e2e/custom_colors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ test.describe('Custom colors for TailwindCSS v4 configuration', () => {
.join('\n');

// Verify xs breakpoint media query is generated (26.25rem = 420px)
// @media(min-width:26.25rem) confirms the xs breakpoint is defined correctly
expect(allCss).toMatch(/@media\(min-width:26\.25rem\)/);
// TailwindCSS v4 outputs range syntax: @media (width>=26.25rem) or @media (width >= 26.25rem)
expect(allCss).toMatch(/@media\s*\(width\s*>=\s*26\.25rem\)/);
});
});
Loading
Loading