Skip to content

Commit 18a32be

Browse files
added playwright fixture to reset db (#98)
* added playwright fixture to reset db * added transactional context to up migrations * added programatic way to setup user and barebone projects e2e test * added support for e2e database and added feedback for minor stuff * synced versions of playwright inside github actions Signed-off-by: Benjamin Strasser <bp.strasser@gmail.com> --------- Signed-off-by: Benjamin Strasser <bp.strasser@gmail.com>
1 parent 416e568 commit 18a32be

File tree

11 files changed

+182
-54
lines changed

11 files changed

+182
-54
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ jobs:
5757
run: pnpm test
5858

5959
- name: Install Playwright Browsers
60-
run: pnpx playwright install --with-deps chromium
60+
run: pnpm exec playwright install --with-deps chromium
6161

6262
- name: Run Playwright tests
6363
run: pnpm test:e2e:only

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ npm-debug.log*
2929
/playwright/.cache/
3030
/playwright-report
3131

32-
db.sqlite
32+
db*.sqlite
3333

3434
/build
3535
/.svelte-kit
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { testWithUser as test } from './fixtures'
2+
3+
test.describe('create project', { tag: ['@foo-bar'] }, () => {
4+
test('projects', async ({ page }) => {
5+
await page.goto('/projects')
6+
// TODO add test
7+
})
8+
})

e2e/specs/fixtures.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { test as base } from '@playwright/test'
2+
import { migrate, undoMigration } from 'services/kysely/migrator.util'
3+
import { setupUser } from './util'
4+
5+
export const test = base.extend({
6+
page: async ({ page }, use) => {
7+
// clean up the database
8+
await undoMigration()
9+
10+
// set up the database
11+
await migrate()
12+
13+
await use(page)
14+
}
15+
})
16+
17+
export const testWithUser = test.extend({
18+
page: async ({ page }, use) => {
19+
await setupUser(page.request)
20+
21+
await use(page)
22+
}
23+
})

e2e/specs/new-user-flow.spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import test, { expect } from '@playwright/test'
1+
import { expect } from '@playwright/test'
2+
import { test } from './fixtures'
3+
import { waitForHydration } from './util'
24

35
test.describe('registration process', { tag: ['@foo-bar'] }, () => {
46
const testEmail = 'foo@bar.com'
57
const testPassword = 'abc123abc123'
68

79
test('registers foo bar and logs into the app', async ({ page, baseURL }) => {
810
await page.goto(`${baseURL!}/signup`)
11+
await waitForHydration(page)
912

1013
await test.step('sign up a new user', async () => {
1114
const firstname = page.getByTestId('signup-firstname-input')
@@ -35,6 +38,7 @@ test.describe('registration process', { tag: ['@foo-bar'] }, () => {
3538

3639
const termsCheckbox = page.getByTestId('signup-terms-checkbox')
3740
await expect(termsCheckbox).toBeVisible()
41+
await termsCheckbox.focus()
3842
await termsCheckbox.check()
3943

4044
const signUpCta = page.getByTestId('signup-cta')

e2e/specs/util.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { type APIRequestContext, type Page, expect } from '@playwright/test'
2+
3+
export async function waitForHydration(page: Page) {
4+
await page.locator('.hydrated').waitFor({ state: 'visible' })
5+
}
6+
7+
export async function setupUser(request: APIRequestContext) {
8+
await register(request)
9+
await login(request)
10+
}
11+
12+
export async function register(request: APIRequestContext) {
13+
const signup = await request.post('/signup', {
14+
headers: {
15+
origin: 'http://localhost:3000'
16+
},
17+
form: {
18+
first_name: 'test',
19+
last_name: 'test',
20+
email: 'test@test.com',
21+
password: 'password',
22+
confirmPassword: 'password',
23+
termsOfService: 'true'
24+
}
25+
})
26+
27+
expect(signup.ok()).toBeTruthy()
28+
}
29+
30+
export async function login(request: APIRequestContext) {
31+
const login = await request.post('/login', {
32+
headers: {
33+
origin: 'http://localhost:3000'
34+
},
35+
form: { email: 'test@test.com', password: 'password' }
36+
})
37+
38+
expect(login.ok()).toBeTruthy()
39+
}

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"private": true,
55
"scripts": {
66
"dev": "pnpm run setup:db && vite dev",
7+
"dev:e2e": "cross-env-shell DATABASE_LOCATION=dbe2e.sqlite \"pnpm run setup:db && vite dev\"",
78
"build": "vite build",
89
"preview": "pnpm run setup:db && vite preview",
910
"sync:svelte": "svelte-kit sync",
@@ -17,12 +18,14 @@
1718
"test:integration": "cross-env DATABASE_LOCATION=:memory: vitest run --project integration",
1819
"test:e2e": "pnpm run build && playwright test",
1920
"test:e2e:only": "playwright test",
21+
"test:e2e:local": "cross-env DATABASE_LOCATION=dbe2e.sqlite playwright test --headed --ui --config ./playwright.config.local.ts",
2022
"---- DB ------------------------------------------------------------": "",
2123
"setup:db": "pnpm migrate:latest && pnpm sync:db",
2224
"migrate": "tsx ./services/src/kysely/migrator.ts",
2325
"migrate:latest": "pnpm run migrate -- latest",
2426
"migrate:up": "pnpm run migrate -- up",
2527
"migrate:down": "pnpm run migrate -- down",
28+
"migrate:reset": "pnpm run migrate -- reset",
2629
"sync:db": "cross-env DATABASE_URL=db.sqlite kysely-codegen",
2730
"---- DOCS ----------------------------------------------------------": "",
2831
"docs:dev": "vitepress dev docs",

playwright.config.local.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { type PlaywrightTestConfig, devices } from '@playwright/test'
2+
3+
const WEB_SERVER_PORT = 3000
4+
const config: PlaywrightTestConfig = {
5+
testDir: './e2e/specs',
6+
outputDir: './e2e/results',
7+
projects: [
8+
{
9+
name: 'Chrome',
10+
testMatch: /.*\.spec\.ts/,
11+
use: {
12+
...devices['Desktop Chrome']
13+
}
14+
}
15+
],
16+
use: {
17+
baseURL: `http://localhost:${WEB_SERVER_PORT}`,
18+
bypassCSP: true
19+
},
20+
webServer: {
21+
command: 'pnpm run dev:e2e',
22+
port: WEB_SERVER_PORT
23+
},
24+
reporter: [['list']]
25+
}
26+
27+
export default config

services/src/kysely/migrations/2024-04-28T09_init.ts

Lines changed: 53 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,61 +2,65 @@ import { Kysely, sql } from 'kysely'
22
import { createTableMigration } from '../migration.util'
33

44
export async function up(db: Kysely<unknown>): Promise<void> {
5-
await createTableMigration(db, 'users')
6-
.addColumn('email', 'text', (col) => col.unique().notNull())
7-
.addColumn('first_name', 'text', (col) => col.notNull())
8-
.addColumn('last_name', 'text', (col) => col.notNull())
9-
.addColumn('role', 'text', (col) => col.defaultTo('user').notNull())
10-
.addColumn('password_hash', 'text', (col) => col.notNull())
11-
.execute()
5+
await db.transaction().execute(async (tx) => {
6+
await createTableMigration(tx, 'users')
7+
.addColumn('email', 'text', (col) => col.unique().notNull())
8+
.addColumn('first_name', 'text', (col) => col.notNull())
9+
.addColumn('last_name', 'text', (col) => col.notNull())
10+
.addColumn('role', 'text', (col) => col.defaultTo('user').notNull())
11+
.addColumn('password_hash', 'text', (col) => col.notNull())
12+
.execute()
1213

13-
await createTableMigration(db, 'projects')
14-
.addColumn('name', 'text', (col) => col.unique().notNull())
15-
.addColumn('base_language', 'integer', (col) =>
16-
col.references('languages.id').onDelete('restrict').notNull()
17-
)
18-
.execute()
14+
await createTableMigration(tx, 'projects')
15+
.addColumn('name', 'text', (col) => col.unique().notNull())
16+
.addColumn('base_language', 'integer', (col) =>
17+
col.references('languages.id').onDelete('restrict').notNull()
18+
)
19+
.execute()
1920

20-
await createTableMigration(db, 'languages')
21-
.addColumn('code', 'text', (col) => col.unique().notNull())
22-
.addColumn('fallback_language', 'integer', (col) => col.references('languages.id'))
23-
.addColumn('project_id', 'integer', (col) =>
24-
col.references('project.id').onDelete('cascade').notNull()
25-
)
26-
.execute()
21+
await createTableMigration(tx, 'languages')
22+
.addColumn('code', 'text', (col) => col.unique().notNull())
23+
.addColumn('fallback_language', 'integer', (col) => col.references('languages.id'))
24+
.addColumn('project_id', 'integer', (col) =>
25+
col.references('project.id').onDelete('cascade').notNull()
26+
)
27+
.execute()
2728

28-
await createTableMigration(db, 'keys')
29-
.addColumn('project_id', 'integer', (col) =>
30-
col.references('projects.id').onDelete('cascade').notNull()
31-
)
32-
.addColumn('name', 'text', (col) => col.unique().notNull())
33-
.execute()
29+
await createTableMigration(tx, 'keys')
30+
.addColumn('project_id', 'integer', (col) =>
31+
col.references('projects.id').onDelete('cascade').notNull()
32+
)
33+
.addColumn('name', 'text', (col) => col.unique().notNull())
34+
.execute()
3435

35-
await createTableMigration(db, 'translations')
36-
.addColumn('key_id', 'integer', (col) =>
37-
col.references('keys.id').onDelete('cascade').notNull()
38-
)
39-
.addColumn('language_id', 'integer', (col) =>
40-
col.references('languages.id').onDelete('cascade').notNull()
41-
)
42-
.addColumn('value', 'text')
43-
.execute()
36+
await createTableMigration(tx, 'translations')
37+
.addColumn('key_id', 'integer', (col) =>
38+
col.references('keys.id').onDelete('cascade').notNull()
39+
)
40+
.addColumn('language_id', 'integer', (col) =>
41+
col.references('languages.id').onDelete('cascade').notNull()
42+
)
43+
.addColumn('value', 'text')
44+
.execute()
4445

45-
await createTableMigration(db, 'projects_users', false, false)
46-
.addColumn('project_id', 'integer', (col) => col.references('projects.id').notNull())
47-
.addColumn('user_id', 'integer', (col) => col.references('user.id').notNull())
48-
.addColumn('permission', 'text', (col) =>
49-
col.check(sql`permission in ('READONLY', 'WRITE', 'ADMIN')`)
50-
)
51-
.addPrimaryKeyConstraint('projects_users_pk', ['project_id', 'user_id'])
52-
.execute()
46+
await createTableMigration(tx, 'projects_users', false, false)
47+
.addColumn('project_id', 'integer', (col) => col.references('projects.id').notNull())
48+
.addColumn('user_id', 'integer', (col) => col.references('user.id').notNull())
49+
.addColumn('permission', 'text', (col) =>
50+
col.check(sql`permission in ('READONLY', 'WRITE', 'ADMIN')`)
51+
)
52+
.addPrimaryKeyConstraint('projects_users_pk', ['project_id', 'user_id'])
53+
.execute()
54+
})
5355
}
5456

5557
export async function down(db: Kysely<unknown>): Promise<void> {
56-
await db.schema.dropTable('users').execute()
57-
await db.schema.dropTable('projects_users').execute()
58-
await db.schema.dropTable('translations').execute()
59-
await db.schema.dropTable('keys').execute()
60-
await db.schema.dropTable('languages').execute()
61-
await db.schema.dropTable('projects').execute()
58+
await db.transaction().execute(async (tx) => {
59+
await tx.schema.dropTable('users').execute()
60+
await tx.schema.dropTable('projects_users').execute()
61+
await tx.schema.dropTable('translations').execute()
62+
await tx.schema.dropTable('keys').execute()
63+
await tx.schema.dropTable('languages').execute()
64+
await tx.schema.dropTable('projects').execute()
65+
})
6266
}

services/src/kysely/migrator.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@ import { MIGRATION_PROVIDER, getMigrator } from './migrator.util'
22
import * as process from 'node:process'
33
import minimist from 'minimist'
44
import { pick } from 'typesafe-utils'
5+
import { NO_MIGRATIONS } from 'kysely'
56

67
function highlight(text: string) {
78
return `\x1b[32m${text}\x1b[0m`
89
}
910

11+
function highlightRed(text: string) {
12+
return `\x1b[31m${text}\x1b[0m`
13+
}
14+
1015
const consoleWithPrefix = (prefix: string): Console =>
1116
new Proxy(global.console, {
1217
get<Target, T extends keyof Target>(target: Target, prop: T) {
@@ -67,12 +72,26 @@ async function main() {
6772
if (!results || !results.length) return console.info(highlight('Database is on base version'))
6873

6974
console.info(
70-
`Migrated to the previous version (DOWN)\n${highlight(results.map(pick('migrationName')).join('\n'))}`
75+
`Migrated to the previous version (DOWN)\n${highlightRed(results.map(pick('migrationName')).join('\n'))}`
7176
)
7277

7378
break
7479
}
7580

81+
case 'reset': {
82+
const { results, error } = await migrator.migrateTo(NO_MIGRATIONS)
83+
84+
if (error) return console.error(error)
85+
if (!results || !results.length) return console.info(highlight('Database is on base version'))
86+
87+
console.info(`Undid all migrations`)
88+
for (const result of results) {
89+
console.info(`${highlightRed(result.migrationName)}`)
90+
}
91+
92+
break
93+
}
94+
7695
default: {
7796
return console.error('Please supply a command')
7897
}

0 commit comments

Comments
 (0)