Skip to content

Commit 9eb566a

Browse files
authored
Merge pull request #210 from zainfathoni/feat/e2e-staging-codegen
feat(e2e): staging auth generation for E2E tests
2 parents 9167ea3 + 38215da commit 9eb566a

10 files changed

Lines changed: 206 additions & 52 deletions

File tree

.beads/.gitignore

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ daemon.log
1111
daemon.pid
1212
bd.sock
1313
sync-state.json
14+
last-touched
1415

1516
# Local version tracking (prevents upgrade notification spam after git ops)
1617
.local_version
@@ -31,11 +32,13 @@ beads.left.meta.json
3132
beads.right.jsonl
3233
beads.right.meta.json
3334

35+
# Sync state (local-only, per-machine)
36+
# These files are machine-specific and should not be shared across clones
37+
.sync.lock
38+
sync_base.jsonl
39+
3440
# NOTE: Do NOT add negation patterns (e.g., !issues.jsonl) here.
3541
# They would override fork protection in .git/info/exclude, allowing
3642
# contributors to accidentally commit upstream issue databases.
3743
# The JSONL files (issues.jsonl, interactions.jsonl) and config files
3844
# are tracked by git by default since no pattern above ignores them.
39-
40-
# Recently accessed files tracking
41-
last-touched

.beads/.sync.lock

Whitespace-only changes.

.beads/sync_base.jsonl

Lines changed: 123 additions & 0 deletions
Large diffs are not rendered by default.

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ Conventional Commits: `<type>[scope]: <description>`
6161

6262
Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
6363

64+
<!-- bv-agent-instructions-v1 -->
65+
6466
## Agent Instructions
6567

6668
Uses **bd** (beads) for issue tracking:
@@ -94,3 +96,5 @@ Work is NOT complete until `git push` succeeds:
9496
- `--id` and `--parent` cannot be used together
9597
- Always add detailed descriptions using `--description` flag
9698
- Always `bd sync` before ending session
99+
100+
<!-- end-bv-agent-instructions -->

app/components/alerts.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CheckCircleIcon } from '@heroicons/react/solid'
1+
import { CheckCircleIcon, XCircleIcon } from '@heroicons/react/solid'
22
import { ReactNode } from 'react'
33

44
export function Alert({ children }: { children: ReactNode }) {
@@ -18,3 +18,18 @@ export function Alert({ children }: { children: ReactNode }) {
1818
</div>
1919
)
2020
}
21+
22+
export function ErrorAlert({ children }: { children: ReactNode }) {
23+
return (
24+
<div className="bg-red-50 border-l-4 border-red-400 py-3 px-4 mt-4">
25+
<div className="flex">
26+
<div className="flex-shrink-0">
27+
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
28+
</div>
29+
<div className="ml-3">
30+
<p className="text-sm font-medium text-red-900">{children}</p>
31+
</div>
32+
</div>
33+
</div>
34+
)
35+
}

app/routes/login.tsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
11
import type { ActionFunction, LoaderFunction } from '@remix-run/node'
22
import { json } from '@remix-run/node'
33
import { Form, useLoaderData, useNavigation } from '@remix-run/react'
4-
import { Alert } from '~/components/alerts'
4+
import { Alert, ErrorAlert } from '~/components/alerts'
55
import { Button } from '~/components/form-elements'
66
import { auth } from '~/services/auth.server'
7-
import { getUserSession } from '~/services/session.server'
7+
import { commitSession, getUserSession } from '~/services/session.server'
88

99
export const loader: LoaderFunction = async ({ request }) => {
1010
await auth.isAuthenticated(request, { successRedirect: '/dashboard' })
1111
const session = await getUserSession(request)
1212
// This session key `auth:magiclink` is the default one used by the EmailLinkStrategy
1313
// you can customize it passing a `sessionMagicLinkKey` when creating an
1414
// instance.
15-
return json({
16-
user: session.get('user'),
17-
magicLinkSent: session.has('zain:magiclink'),
18-
})
15+
const error = session.get(auth.sessionErrorKey) as
16+
| { message: string }
17+
| undefined
18+
return json(
19+
{
20+
user: session.get('user'),
21+
magicLinkSent: session.has('zain:magiclink'),
22+
error: error?.message,
23+
},
24+
{
25+
headers: {
26+
'Set-Cookie': await commitSession(session),
27+
},
28+
}
29+
)
1930
}
2031

2132
export const action: ActionFunction = async ({ request }) => {
@@ -31,7 +42,10 @@ export const action: ActionFunction = async ({ request }) => {
3142
}
3243

3344
export default function Login() {
34-
const { magicLinkSent } = useLoaderData<{ magicLinkSent: boolean }>()
45+
const { magicLinkSent, error } = useLoaderData<{
46+
magicLinkSent: boolean
47+
error?: string
48+
}>()
3549
const { state } = useNavigation()
3650

3751
return (
@@ -79,6 +93,7 @@ export default function Login() {
7993
</Button>
8094
</div>
8195
</Form>
96+
{error ? <ErrorAlert>{error}</ErrorAlert> : null}
8297
{magicLinkSent ? (
8398
<Form action="/logout" method="post">
8499
<input type="hidden" name="redirectTo" value="/login" />

app/services/session.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const sessionStorage = createCookieSessionStorage({
77
sameSite: 'lax',
88
path: '/',
99
httpOnly: true,
10-
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
10+
maxAge: 7 * 24 * 60 * 60, // 7 days in seconds
1111
secrets: [getRequiredServerEnvVar('SESSION_SECRET')],
1212
// normally you want this to be `secure: true`
1313
// but that doesn't work on localhost for Safari

docs/testing-principles.md

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -227,36 +227,26 @@ npm run test:e2e:staging
227227

228228
#### Setting Up Staging Authentication
229229

230-
Since staging uses real email delivery, auth fixtures must be created manually:
231-
232-
1. Login to https://staging.kelas.rumahberbagi.com in your browser
233-
2. Open DevTools > Application > Storage > Cookies
234-
3. Export cookies using a browser extension (e.g., "EditThisCookie")
235-
4. Save to `e2e/fixtures/auth/staging/<role>.staging.json` in this format:
236-
237-
```json
238-
{
239-
"cookies": [
240-
{
241-
"name": "__session",
242-
"value": "...",
243-
"domain": "staging.kelas.rumahberbagi.com",
244-
"path": "/",
245-
"expires": -1,
246-
"httpOnly": true,
247-
"secure": true,
248-
"sameSite": "Lax"
249-
}
250-
],
251-
"origins": []
252-
}
230+
Since staging uses real email delivery, auth fixtures are created using npm
231+
scripts that run Playwright codegen:
232+
233+
```bash
234+
npm run test:e2e:staging:auth:member # Login with: member@rumahberbagi.com
235+
npm run test:e2e:staging:auth:author # Login with: vika@rumahberbagi.com
236+
npm run test:e2e:staging:auth:admin # Login with: admin@rumahberbagi.com
253237
```
254238

239+
1. Run the npm script for the role you need
240+
2. Login with the email shown in the terminal
241+
3. Close the browser - storage state is saved automatically
242+
255243
Required auth fixtures:
256244

257-
- `member.staging.json` - Regular member
258-
- `author.staging.json` - Course author
259-
- `admin.staging.json` - Administrator
245+
| Fixture | Email | Role |
246+
| --------------------- | ----------------------- | -------------- |
247+
| `member.staging.json` | member@rumahberbagi.com | Regular member |
248+
| `author.staging.json` | vika@rumahberbagi.com | Course author |
249+
| `admin.staging.json` | admin@rumahberbagi.com | Administrator |
260250

261251
**Note:** Auth fixtures are gitignored and must be recreated when sessions
262252
expire.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
"test:e2e:run": "cross-env RUNNING_E2E=true DATABASE_URL=file:./test.db start-server-and-test start:e2e http-get://localhost:3000/ test:e2e",
3030
"test:e2e:docker": "playwright test --config=playwright.docker.config.ts",
3131
"test:e2e:staging": "playwright test --config=playwright.staging.config.ts",
32+
"test:e2e:staging:auth:member": "echo 'Login with: member@rumahberbagi.com' && playwright codegen https://staging.kelas.rumahberbagi.com --save-storage=e2e/fixtures/auth/staging/member.staging.json",
33+
"test:e2e:staging:auth:author": "echo 'Login with: vika@rumahberbagi.com' && playwright codegen https://staging.kelas.rumahberbagi.com --save-storage=e2e/fixtures/auth/staging/author.staging.json",
34+
"test:e2e:staging:auth:admin": "echo 'Login with: admin@rumahberbagi.com' && playwright codegen https://staging.kelas.rumahberbagi.com --save-storage=e2e/fixtures/auth/staging/admin.staging.json",
3235
"test:e2e:production": "cross-env BASE_URL=https://kelas.rumahberbagi.com playwright test --config=playwright.docker.config.ts",
3336
"type-check": "tsc --noEmit",
3437
"setup": "npm install && prisma migrate reset --force && npm run test:e2e:run",

playwright-staging-setup.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,17 @@ const REQUIRED_AUTH_FILES = [
1313
*
1414
* Unlike local tests, staging authentication cannot be automated via magic link
1515
* because emails are sent to real addresses. Instead, this setup validates that
16-
* manually-exported auth fixtures exist.
16+
* auth fixtures exist, which are generated using Playwright codegen.
1717
*
18-
* To create auth fixtures for staging:
19-
* 1. Login to https://staging.kelas.rumahberbagi.com in your browser
20-
* 2. Open DevTools > Application > Storage > Cookies
21-
* 3. Export the cookies using browser extension or manually copy them
22-
* 4. Save to e2e/fixtures/auth/staging/<role>.staging.json in Playwright's
23-
* storageState format:
24-
* {
25-
* "cookies": [{ "name": "...", "value": "...", "domain": "...", ... }],
26-
* "origins": []
27-
* }
18+
* To create auth fixtures for staging, use the npm scripts:
19+
*
20+
* npm run test:e2e:staging:auth:member # Login with: member@rumahberbagi.com
21+
* npm run test:e2e:staging:auth:author # Login with: vika@rumahberbagi.com
22+
* npm run test:e2e:staging:auth:admin # Login with: admin@rumahberbagi.com
23+
*
24+
* 1. Run the npm script for the role you need
25+
* 2. Login with the email shown above in the browser that opens
26+
* 3. Close the browser - storage state is saved automatically
2827
*/
2928
async function globalSetup() {
3029
if (!fs.existsSync(STAGING_AUTH_DIR)) {
@@ -46,9 +45,11 @@ async function globalSetup() {
4645
for (const file of missingFiles) {
4746
console.warn(` - ${STAGING_AUTH_DIR}/${file}`)
4847
}
49-
console.warn(
50-
'\nSee playwright-staging-setup.ts for instructions on creating these files.\n'
51-
)
48+
console.warn('\nGenerate with: npm run test:e2e:staging:auth:<role>\n')
49+
console.warn('Staging emails:')
50+
console.warn(' member: member@rumahberbagi.com')
51+
console.warn(' author: vika@rumahberbagi.com')
52+
console.warn(' admin: admin@rumahberbagi.com\n')
5253
}
5354

5455
console.log('✓ Staging global setup complete')

0 commit comments

Comments
 (0)