@live-change/user-frontend 0.9.203 → 0.9.205

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.node-version +1 -0
  2. package/.nvmrc +1 -0
  3. package/dist/server/app.config.d.ts +177 -0
  4. package/dist/server/app.config.js +135 -0
  5. package/dist/server/init-functions.d.ts +6 -0
  6. package/dist/server/init-functions.js +20 -0
  7. package/dist/server/init.d.ts +1 -0
  8. package/dist/server/init.js +46 -0
  9. package/dist/server/security.config.d.ts +47 -0
  10. package/dist/server/security.config.js +48 -0
  11. package/dist/server/services.list.d.ts +21 -0
  12. package/dist/server/services.list.js +24 -0
  13. package/dist/server/start.d.ts +1 -0
  14. package/dist/server/start.js +32 -0
  15. package/dist/tsconfig.tsbuildinfo +1 -0
  16. package/e2e/connectEmailCode.test.ts +11 -7
  17. package/e2e/connectEmailLink.test.ts +12 -6
  18. package/e2e/delete.test.ts +6 -3
  19. package/e2e/disconnectEmail.test.ts +11 -12
  20. package/e2e/e2eSuite.ts +8 -12
  21. package/e2e/env.ts +56 -58
  22. package/e2e/execution-time-report.ts +7 -0
  23. package/e2e/resetPasswordWithEmailCode.test.ts +8 -6
  24. package/e2e/resetPasswordWithEmailLink.test.ts +8 -6
  25. package/e2e/runner.ts +10 -0
  26. package/e2e/setPassword.test.ts +19 -12
  27. package/e2e/signInEmailCode.test.ts +4 -2
  28. package/e2e/signInEmailLink.test.ts +5 -3
  29. package/e2e/signInEmailPassword.test.ts +3 -2
  30. package/e2e/signOut.test.ts +5 -2
  31. package/e2e/signUpEmailCode.test.ts +5 -2
  32. package/e2e/signUpEmailLink.test.ts +5 -3
  33. package/e2e/steps.ts +57 -21
  34. package/e2e/withBrowser.ts +2 -16
  35. package/front/src/NavBar.vue +1 -1
  36. package/front/src/connected/Connected.vue +2 -1
  37. package/front/src/message-auth/MessageLink.vue +5 -2
  38. package/front/src/message-auth/MessageSent.vue +2 -3
  39. package/front/src/message-auth/email/ConnectEmail.vue +1 -5
  40. package/front/src/message-auth/email/ResetPasswordEmail.vue +1 -5
  41. package/front/src/message-auth/email/SignInEmail.vue +1 -5
  42. package/front/src/message-auth/sms/ResetPasswordSms.vue +1 -5
  43. package/front/src/message-auth/sms/SignInSms.vue +1 -5
  44. package/front/src/message-auth/sms/SignUpSms.vue +1 -5
  45. package/front/src/password/ResetPasswordForm.vue +3 -1
  46. package/package.json +37 -29
@@ -1,10 +1,10 @@
1
- import test from 'node:test'
1
+ import { e2eSuite, test } from './e2eSuite.js'
2
+ import { waitForHydration } from '@live-change/e2e-test'
2
3
  import assert from 'node:assert'
3
4
  import App from '@live-change/framework'
4
5
  import randomProfile from 'random-profile-generator'
5
6
  import { withBrowser } from './withBrowser.js'
6
- import { useSecretCode, sleep } from './steps.js'
7
- import { e2eSuite } from './e2eSuite.js'
7
+ import { useSecretCode } from './steps.js'
8
8
 
9
9
  const app = App.app()
10
10
  const email = randomProfile.profile().firstName.toLowerCase() + '@test.com'
@@ -23,6 +23,7 @@ e2eSuite('connectEmailCode', () => {
23
23
  await User.create({ id: user, roles: [] })
24
24
  await Email.create({ id: email, email, user })
25
25
  await page.goto(env.url + '/', { waitUntil: 'networkidle' })
26
+ await waitForHydration(page)
26
27
  const session = await page.evaluate(
27
28
  () => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session
28
29
  )
@@ -30,17 +31,19 @@ e2eSuite('connectEmailCode', () => {
30
31
 
31
32
  // Reload so client gets user from server; otherwise router redirects to sign-in when opening /user/settings/connected
32
33
  await page.reload({ waitUntil: 'networkidle' })
34
+ await waitForHydration(page)
33
35
  await page.goto(env.url + '/user/settings/connected', { waitUntil: 'networkidle' })
36
+ await waitForHydration(page)
34
37
  await page.getByText(email).waitFor({ state: 'visible', timeout: 15000 })
35
38
  assert.strictEqual(await page.getByText(email2).isVisible(), false, 'email2 should not be visible')
36
39
 
37
- await page.click('button#connect')
38
- assert.ok(page.url().includes('/connect'))
40
+ await page.getByRole('link', { name: /add email/i }).first().click()
41
+ await page.waitForURL(/connect-email/, { timeout: 15000 })
39
42
 
40
43
  if (!happyPath) {
41
44
  await page.fill('input#email', email)
42
45
  await page.click('button[type=submit]')
43
- assert.ok(page.url().includes('/connect'))
46
+ assert.ok(page.url().includes('connect-email'))
44
47
  }
45
48
 
46
49
  await page.fill('input#email', email2)
@@ -56,11 +59,12 @@ e2eSuite('connectEmailCode', () => {
56
59
  assert.strictEqual((authenticationData as { messageData?: { user: string } })?.messageData?.user, user, 'authentication contains user')
57
60
 
58
61
  await useSecretCode(page, env, authentication, happyPath)
59
- await page.waitForURL('**/connect-finished', { timeout: 10000 })
62
+ await page.waitForURL('**/connect-finished', { timeout: 30000 })
60
63
  assert.ok(page.url().includes('/connect-finished'))
61
64
 
62
65
  if (!happyPath) {
63
66
  await page.goto(url, { waitUntil: 'networkidle' })
67
+ await waitForHydration(page)
64
68
  assert.ok(page.url().includes('/connect-finished'))
65
69
  }
66
70
  })
@@ -1,10 +1,10 @@
1
- import test from 'node:test'
1
+ import { e2eSuite, test } from './e2eSuite.js'
2
+ import { waitForHydration } from '@live-change/e2e-test'
2
3
  import assert from 'node:assert'
3
4
  import App from '@live-change/framework'
4
5
  import randomProfile from 'random-profile-generator'
5
6
  import { withBrowser } from './withBrowser.js'
6
7
  import { useSecretLink } from './steps.js'
7
- import { e2eSuite } from './e2eSuite.js'
8
8
 
9
9
  const app = App.app()
10
10
  const email = randomProfile.profile().firstName.toLowerCase() + '@test.com'
@@ -23,26 +23,31 @@ e2eSuite('connectEmailLink', () => {
23
23
  await User.create({ id: user, roles: [] })
24
24
  await Email.create({ id: email, email, user })
25
25
  await page.goto(env.url + '/', { waitUntil: 'networkidle' })
26
+ await waitForHydration(page)
26
27
  const session = await page.evaluate(
27
28
  () => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session
28
29
  )
29
30
  await AuthenticatedUser.create({ id: session, user, session })
30
31
 
31
32
  await page.reload({ waitUntil: 'networkidle' })
33
+ await waitForHydration(page)
32
34
  await page.goto(env.url + '/user/settings/connected', { waitUntil: 'networkidle' })
35
+ await waitForHydration(page)
33
36
  await page.getByText(email).waitFor({ state: 'visible', timeout: 15000 })
34
37
  assert.strictEqual(await page.getByText(email2).isVisible(), false, 'email2 should not be visible')
35
-
36
- await page.click('button#connect')
37
- assert.ok(page.url().includes('/connect'))
38
+
39
+ await page.locator('[data-testid="connect-email"]').first().click()
40
+ await page.waitForURL(/connect-email/, { timeout: 15000 })
38
41
 
39
42
  if (!happyPath) {
40
43
  await page.fill('input#email', email)
41
44
  await page.click('button[type=submit]')
45
+ assert.ok(page.url().includes('connect-email'))
42
46
  }
43
47
 
44
48
  await page.fill('input#email', email2)
45
49
  await page.click('button[type=submit]')
50
+ await page.waitForURL('**/sent/*', { timeout: 10000 })
46
51
  assert.ok(page.url().includes('/sent/'))
47
52
 
48
53
  const url = page.url()
@@ -53,11 +58,12 @@ e2eSuite('connectEmailLink', () => {
53
58
  assert.strictEqual((authenticationData as { messageData?: { user: string } })?.messageData?.user, user, 'authentication contains user')
54
59
 
55
60
  const linkData = await useSecretLink(page, env, authentication, happyPath)
56
- await page.waitForURL('**/connect-finished', { timeout: 10000 })
61
+ await page.waitForURL('**/connect-finished', { timeout: 30000 })
57
62
  assert.ok(page.url().includes('/connect-finished'))
58
63
 
59
64
  if (!happyPath) {
60
65
  await page.goto(env.url + '/user/link/' + linkData.secretCode, { waitUntil: 'networkidle' })
66
+ await waitForHydration(page)
61
67
  await page.getByText('Link used').waitFor({ state: 'visible' })
62
68
  }
63
69
  })
@@ -1,9 +1,9 @@
1
- import test from 'node:test'
1
+ import { e2eSuite, test } from './e2eSuite.js'
2
+ import { waitForHydration } from '@live-change/e2e-test'
2
3
  import assert from 'node:assert'
3
4
  import App from '@live-change/framework'
4
5
  import randomProfile from 'random-profile-generator'
5
6
  import { withBrowser } from './withBrowser.js'
6
- import { e2eSuite } from './e2eSuite.js'
7
7
 
8
8
  const app = App.app()
9
9
  const name = randomProfile.profile().firstName.toLowerCase()
@@ -21,19 +21,22 @@ e2eSuite('delete', () => {
21
21
  await User.create({ id: user, roles: [] })
22
22
  await Email.create({ id: email, email, user })
23
23
  await page.goto(env.url + '/', { waitUntil: 'networkidle' })
24
+ await waitForHydration(page)
24
25
  const session = await page.evaluate(
25
26
  () => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session
26
27
  )
27
28
  await AuthenticatedUser.create({ id: session, user, session })
28
29
 
29
30
  await page.reload({ waitUntil: 'networkidle' })
31
+ await waitForHydration(page)
30
32
  const clientUser = await page.evaluate(
31
33
  () => (window as unknown as { api: { client: { value: { user: string } } } }).api.client.value.user
32
34
  )
33
35
  assert.strictEqual(user, clientUser, 'client logged in')
34
36
 
35
37
  await page.goto(env.url + '/user/settings/delete', { waitUntil: 'networkidle' })
36
- await page.click('.p-checkbox-box')
38
+ await waitForHydration(page)
39
+ await page.getByRole('checkbox').check()
37
40
  await page.click('button#delete')
38
41
  await page.waitForURL('**/delete-finished', { timeout: 10000 })
39
42
 
@@ -1,15 +1,14 @@
1
- import test from 'node:test'
1
+ import { e2eSuite, test } from './e2eSuite.js'
2
+ import { waitForHydration } from '@live-change/e2e-test'
2
3
  import assert from 'node:assert'
3
4
  import App from '@live-change/framework'
4
5
  import randomProfile from 'random-profile-generator'
5
6
  import { withBrowser } from './withBrowser.js'
6
- import { e2eSuite } from './e2eSuite.js'
7
7
 
8
8
  const app = App.app()
9
9
  const name = randomProfile.profile().firstName.toLowerCase()
10
10
  const email = name + '@test.com'
11
11
  const email2 = name + '2@test.com'
12
- const happyPath = false
13
12
 
14
13
  e2eSuite('disconnectEmail', () => {
15
14
  test('disconnect email', async () => {
@@ -24,23 +23,23 @@ e2eSuite('disconnectEmail', () => {
24
23
  await Email.create({ id: email, email, user })
25
24
  await Email.create({ id: email2, email: email2, user })
26
25
  await page.goto(env.url + '/', { waitUntil: 'networkidle' })
26
+ await waitForHydration(page)
27
27
  const session = await page.evaluate(
28
28
  () => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session
29
29
  )
30
30
  await AuthenticatedUser.create({ id: session, user, session })
31
31
 
32
32
  await page.reload({ waitUntil: 'networkidle' })
33
+ await waitForHydration(page)
33
34
  await page.goto(env.url + '/user/settings/connected', { waitUntil: 'networkidle' })
34
- await page.getByText(email).waitFor({ state: 'visible' })
35
- await page.getByText(email2).waitFor({ state: 'visible' })
35
+ await waitForHydration(page)
36
+ await page.getByText(email, { exact: true }).waitFor({ state: 'visible' })
37
+ await page.getByText(email2, { exact: true }).waitFor({ state: 'visible' })
36
38
 
37
- await page.click('span.pi-times')
38
- await page.click('text=Yes')
39
- assert.strictEqual(await page.getByText(email2).isVisible(), false, 'email2 should not be visible')
40
-
41
- if (!happyPath) {
42
- await page.locator('span.pi-times').waitFor({ state: 'hidden' })
43
- }
39
+ await page.locator('li').filter({ hasText: email }).locator('button.p-button-rounded').click()
40
+ await page.locator('[role="alertdialog"] button.p-button-danger').last().click({ force: true })
41
+ assert.strictEqual(await page.getByText(email, { exact: true }).isVisible(), false, 'disconnected email should be gone')
42
+ assert.strictEqual(await page.getByText(email2, { exact: true }).isVisible(), true, 'other email should remain')
44
43
  })
45
44
  })
46
45
  })
package/e2e/e2eSuite.ts CHANGED
@@ -1,12 +1,8 @@
1
- import { after, describe } from 'node:test'
2
- import { disposeTestEnv } from './env.js'
3
-
4
- export function e2eSuite(name: string, define: () => void): void {
5
- describe(name, () => {
6
- after(async () => {
7
- await disposeTestEnv()
8
- process.exit(0)
9
- })
10
- define()
11
- })
12
- }
1
+ export {
2
+ e2eSuite,
3
+ getE2ERegistry,
4
+ resetE2ERegistry,
5
+ setCurrentE2EFile,
6
+ test,
7
+ type E2ETestDefinition
8
+ } from '@live-change/e2e-test'
package/e2e/env.ts CHANGED
@@ -1,25 +1,13 @@
1
1
  import path from 'path'
2
2
  import { fileURLToPath } from 'url'
3
+ import App from '@live-change/framework'
3
4
  import { TestServer } from '@live-change/server'
5
+ import { createTestEnvHelpers, waitForServerReady } from '@live-change/e2e-test'
4
6
  import appConfig from '../server/app.config.js'
5
7
  import * as services from '../server/services.list.js'
6
8
 
7
- const READY_TIMEOUT_MS = 60000
8
- const READY_POLL_MS = 2000
9
-
10
- async function waitForServerReady(url: string): Promise<void> {
11
- const deadline = Date.now() + READY_TIMEOUT_MS
12
- while (Date.now() < deadline) {
13
- try {
14
- const res = await fetch(url)
15
- if (res.ok) return
16
- } catch {
17
- // not ready yet
18
- }
19
- await new Promise((r) => setTimeout(r, READY_POLL_MS))
20
- }
21
- throw new Error(`Server at ${url} did not become ready within ${READY_TIMEOUT_MS}ms`)
22
- }
9
+ const appRuntime = App.app()
10
+ const internalE2EClient = { internal: true, roles: ['admin'] as const }
23
11
 
24
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
25
13
  const serverDir = path.join(__dirname, '..', 'server')
@@ -29,16 +17,34 @@ for (const serviceConfig of appConfig.services) {
29
17
  const name = (serviceConfig as { name: string }).name
30
18
  ;(serviceConfig as { module?: unknown }).module = (services as Record<string, unknown>)[name]
31
19
  }
32
- ;(appConfig as { init?: (s: unknown) => Promise<void> }).init = (services as { init: (s: unknown) => Promise<void> }).init
20
+ ;(appConfig as { init?: (s: unknown) => Promise<void> }).init =
21
+ (services as { init: (s: unknown) => Promise<void> }).init
33
22
 
34
23
  export type TestEnv = {
35
24
  server: InstanceType<typeof TestServer>
36
25
  url: string
37
- haveService: (name: string) => { name: string; models: Record<string, { get: (id: string) => Promise<unknown> }>; views: Record<string, unknown>; actions: Record<string, unknown>; triggers: Record<string, unknown> }
38
- haveModel: (serviceName: string, modelName: string) => { get: (id: string) => Promise<unknown>; create: (data: unknown) => Promise<unknown>; update: (id: string, data: unknown) => Promise<unknown>; delete: (id: string) => Promise<unknown>; indexObjectGet: (index: string, key: unknown, opts?: unknown) => Promise<unknown>; indexRangeGet: (index: string, key: unknown) => Promise<unknown[]>; definition: { properties: Record<string, { preFilter: (v: unknown) => unknown }> } }
26
+ haveService: (name: string) => {
27
+ name: string
28
+ models: Record<string, { get: (id: string) => Promise<unknown> }>
29
+ views: Record<string, unknown>
30
+ actions: Record<string, unknown>
31
+ triggers: Record<string, unknown>
32
+ }
33
+ haveModel: (
34
+ serviceName: string,
35
+ modelName: string
36
+ ) => {
37
+ get: (id: string) => Promise<unknown>
38
+ create: (data: unknown) => Promise<unknown>
39
+ update: (id: string, data: unknown) => Promise<unknown>
40
+ delete: (id: string) => Promise<unknown>
41
+ indexObjectGet: (index: string, key: unknown, opts?: unknown) => Promise<unknown>
42
+ indexRangeGet: (index: string, key: unknown) => Promise<unknown[]>
43
+ definition: { properties: Record<string, { preFilter: (v: unknown) => unknown }> }
44
+ }
39
45
  haveView: (serviceName: string, viewName: string) => unknown
40
- haveAction: (serviceName: string, actionName: string) => unknown
41
- haveTrigger: (serviceName: string, triggerName: string) => unknown
46
+ haveAction: (serviceName: string, actionName: string) => (data: unknown) => Promise<unknown>
47
+ haveTrigger: (serviceName: string, triggerName: string) => (data: unknown) => Promise<unknown>
42
48
  grabObject: (serviceName: string, modelName: string, id: string) => Promise<unknown>
43
49
  }
44
50
 
@@ -48,23 +54,11 @@ let envPromise: Promise<TestEnv> | null = null
48
54
  let testServer: TestServerInstance | null = null
49
55
 
50
56
  export async function disposeTestEnv(): Promise<void> {
51
- const server = testServer
57
+ const s = testServer
58
+ if (!s) return
52
59
  testServer = null
53
60
  envPromise = null
54
- if (server) await server.dispose()
55
- }
56
-
57
- function haveService(server: InstanceType<typeof TestServer>, name: string) {
58
- const service = server.apiServer.services.services.find((s: { name: string }) => s.name === name)
59
- if (!service) throw new Error('service ' + name + ' not found')
60
- return service
61
- }
62
-
63
- function haveModel(server: InstanceType<typeof TestServer>, serviceName: string, modelName: string) {
64
- const service = haveService(server, serviceName)
65
- const model = service.models[modelName]
66
- if (!model) throw new Error('model ' + modelName + ' not found')
67
- return model
61
+ await s.dispose()
68
62
  }
69
63
 
70
64
  export async function getTestEnv(): Promise<TestEnv> {
@@ -96,33 +90,37 @@ export async function getTestEnv(): Promise<TestEnv> {
96
90
  })
97
91
 
98
92
  const url = server.url!
93
+ const helpers = createTestEnvHelpers(server)
99
94
  return {
100
95
  server,
101
96
  url,
102
- haveService: (name: string) => haveService(server, name),
103
- haveModel: (serviceName: string, modelName: string) => haveModel(server, serviceName, modelName),
104
- haveView: (serviceName: string, viewName: string) => {
105
- const service = haveService(server, serviceName)
106
- const view = service.views[viewName]
107
- if (!view) throw new Error('view ' + viewName + ' not found')
108
- return view
109
- },
110
- haveAction: (serviceName: string, actionName: string) => {
111
- const service = haveService(server, serviceName)
112
- const action = service.actions[actionName]
113
- if (!action) throw new Error('action ' + actionName + ' not found')
114
- return action
97
+ haveService: helpers.haveService,
98
+ haveModel: helpers.haveModel,
99
+ haveView: helpers.haveView,
100
+ haveAction(serviceName: string, actionName: string) {
101
+ const action = helpers.haveAction(serviceName, actionName) as {
102
+ callCommand: (parameters: unknown, clientData: unknown) => Promise<unknown>
103
+ }
104
+ return (parameters: unknown) => {
105
+ const p = parameters as Record<string, unknown> & { _e2eGrantUser?: string }
106
+ const { _e2eGrantUser, ...commandParams } = p
107
+ const client = _e2eGrantUser
108
+ ? { ...internalE2EClient, user: _e2eGrantUser as string }
109
+ : internalE2EClient
110
+ return action.callCommand(commandParams, client).then((res: unknown) => {
111
+ if (typeof res === 'string') return { id: res }
112
+ return res
113
+ })
114
+ }
115
115
  },
116
- haveTrigger: (serviceName: string, triggerName: string) => {
117
- const service = haveService(server, serviceName)
118
- const trigger = service.triggers[triggerName]
119
- if (!trigger) throw new Error('trigger ' + triggerName + ' not found')
120
- return trigger
116
+ haveTrigger(serviceName: string, triggerName: string) {
117
+ return (data: unknown) =>
118
+ appRuntime.triggerService(
119
+ { service: serviceName, type: triggerName, client: internalE2EClient },
120
+ data as Record<string, unknown>
121
+ )
121
122
  },
122
- grabObject: async (serviceName: string, modelName: string, id: string) => {
123
- const model = haveModel(server, serviceName, modelName)
124
- return await model.get(id)
125
- }
123
+ grabObject: helpers.grabObject
126
124
  }
127
125
  })()
128
126
  return envPromise
@@ -0,0 +1,7 @@
1
+ import { runE2E } from './runner.js'
2
+
3
+ runE2E(['--report-time', 'e2e/*.test.ts'])
4
+ .then((code) => process.exit(code))
5
+ .catch((error) => {
6
+ throw error
7
+ })
@@ -1,11 +1,11 @@
1
- import test from 'node:test'
1
+ import { e2eSuite, test } from './e2eSuite.js'
2
+ import { waitForHydration } from '@live-change/e2e-test'
2
3
  import assert from 'node:assert'
3
4
  import App from '@live-change/framework'
4
5
  import randomProfile from 'random-profile-generator'
5
6
  import passwordGenerator from 'generate-password'
6
7
  import { withBrowser } from './withBrowser.js'
7
- import { useSecretCode } from './steps.js'
8
- import { e2eSuite } from './e2eSuite.js'
8
+ import { setPrimePasswordFieldValue, useSecretCode } from './steps.js'
9
9
 
10
10
  const app = App.app()
11
11
  const email = randomProfile.profile().firstName.toLowerCase() + '@test.com'
@@ -21,8 +21,10 @@ e2eSuite('resetPasswordWithEmailCode', () => {
21
21
  await User.create({ id: user, roles: [] })
22
22
  await Email.create({ id: email, email, user })
23
23
  await page.goto(env.url + '/', { waitUntil: 'networkidle' })
24
+ await waitForHydration(page)
24
25
 
25
26
  await page.goto(env.url + '/user/reset-password', { waitUntil: 'networkidle' })
27
+ await waitForHydration(page)
26
28
  await page.fill('input#email', email)
27
29
  await page.click('button[type=submit]')
28
30
  await page.waitForURL('**/sent/*', { timeout: 10000 })
@@ -49,12 +51,12 @@ e2eSuite('resetPasswordWithEmailCode', () => {
49
51
  const resetPasswordAuthenticationData = await ResetPasswordAuthentication.indexObjectGet('byKey', resetPasswordAuthentication)
50
52
  assert.ok(resetPasswordAuthenticationData, 'reset password authentication created')
51
53
 
52
- await page.getByText('Reset password').waitFor({ state: 'visible' })
54
+ await page.getByText('Reset password', { exact: false }).first().waitFor({ state: 'visible' })
53
55
 
54
56
  const password =
55
57
  passwordGenerator.generate({ length: 10, numbers: true }) + (Math.random() * 10).toFixed()
56
- await page.locator('input[type="password"]').nth(0).fill(password)
57
- await page.locator('input[type="password"]').nth(1).fill(password)
58
+ await setPrimePasswordFieldValue(page, '#newPassword', password)
59
+ await setPrimePasswordFieldValue(page, '#reenterPassword', password)
58
60
  await page.click('button[type=submit]')
59
61
  await page.waitForURL('**/reset-password-finished', { timeout: 10000 })
60
62
  assert.ok(page.url().includes('/reset-password-finished'))
@@ -1,11 +1,11 @@
1
- import test from 'node:test'
1
+ import { e2eSuite, test } from './e2eSuite.js'
2
+ import { waitForHydration } from '@live-change/e2e-test'
2
3
  import assert from 'node:assert'
3
4
  import App from '@live-change/framework'
4
5
  import randomProfile from 'random-profile-generator'
5
6
  import passwordGenerator from 'generate-password'
6
7
  import { withBrowser } from './withBrowser.js'
7
- import { useSecretLink } from './steps.js'
8
- import { e2eSuite } from './e2eSuite.js'
8
+ import { setPrimePasswordFieldValue, useSecretLink } from './steps.js'
9
9
 
10
10
  const app = App.app()
11
11
  const email = randomProfile.profile().firstName.toLowerCase() + '@test.com'
@@ -21,8 +21,10 @@ e2eSuite('resetPasswordWithEmailLink', () => {
21
21
  await User.create({ id: user, roles: [] })
22
22
  await Email.create({ id: email, email, user })
23
23
  await page.goto(env.url + '/', { waitUntil: 'networkidle' })
24
+ await waitForHydration(page)
24
25
 
25
26
  await page.goto(env.url + '/user/reset-password', { waitUntil: 'networkidle' })
27
+ await waitForHydration(page)
26
28
  await page.fill('input#email', email)
27
29
  await page.click('button[type=submit]')
28
30
  await page.waitForURL('**/sent/*', { timeout: 10000 })
@@ -49,12 +51,12 @@ e2eSuite('resetPasswordWithEmailLink', () => {
49
51
  const resetPasswordAuthenticationData = await ResetPasswordAuthentication.indexObjectGet('byKey', resetPasswordAuthentication)
50
52
  assert.ok(resetPasswordAuthenticationData, 'reset password authentication created')
51
53
 
52
- await page.getByText('Reset password').waitFor({ state: 'visible' })
54
+ await page.getByText('Reset password', { exact: false }).first().waitFor({ state: 'visible' })
53
55
 
54
56
  const password =
55
57
  passwordGenerator.generate({ length: 10, numbers: true }) + (Math.random() * 10).toFixed()
56
- await page.locator('input[type="password"]').nth(0).fill(password)
57
- await page.locator('input[type="password"]').nth(1).fill(password)
58
+ await setPrimePasswordFieldValue(page, '#newPassword', password)
59
+ await setPrimePasswordFieldValue(page, '#reenterPassword', password)
58
60
  await page.click('button[type=submit]')
59
61
  await page.waitForURL('**/reset-password-finished', { timeout: 10000 })
60
62
  assert.ok(page.url().includes('/reset-password-finished'))
package/e2e/runner.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { createRunner } from '@live-change/e2e-test'
2
+ import { disposeTestEnv, getTestEnv } from './env.js'
3
+
4
+ const runner = createRunner({
5
+ setupEnv: getTestEnv,
6
+ teardownEnv: disposeTestEnv
7
+ })
8
+
9
+ export const runE2E = runner.runE2E
10
+ await runner.runCli(import.meta.url, process.argv.slice(2))
@@ -1,10 +1,11 @@
1
- import test from 'node:test'
1
+ import { e2eSuite, test } from './e2eSuite.js'
2
+ import { waitForHydration } from '@live-change/e2e-test'
2
3
  import assert from 'node:assert'
3
4
  import App from '@live-change/framework'
4
5
  import randomProfile from 'random-profile-generator'
5
6
  import passwordGenerator from 'generate-password'
7
+ import { setPrimePasswordFieldValue } from './steps.js'
6
8
  import { withBrowser } from './withBrowser.js'
7
- import { e2eSuite } from './e2eSuite.js'
8
9
 
9
10
  const app = App.app()
10
11
  const name = randomProfile.profile().firstName.toLowerCase()
@@ -22,12 +23,14 @@ e2eSuite('setPassword', () => {
22
23
  await User.create({ id: user, roles: [] })
23
24
  await Email.create({ id: email, email, user })
24
25
  await page.goto(env.url + '/', { waitUntil: 'networkidle' })
26
+ await waitForHydration(page)
25
27
  const session = await page.evaluate(
26
28
  () => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session
27
29
  )
28
30
  await AuthenticatedUser.create({ id: session, user, session })
29
31
 
30
32
  await page.reload({ waitUntil: 'networkidle' })
33
+ await waitForHydration(page)
31
34
  const clientUser = await page.evaluate(
32
35
  () => (window as unknown as { api: { client: { value: { user: string } } } }).api.client.value.user
33
36
  )
@@ -37,28 +40,32 @@ e2eSuite('setPassword', () => {
37
40
  assert.strictEqual(emptyPasswordAuthenticationData, null, 'password not set')
38
41
 
39
42
  await page.goto(env.url + '/user/settings/change-password', { waitUntil: 'networkidle' })
40
- await page.getByText('Set password').waitFor({ state: 'visible' })
43
+ await waitForHydration(page)
44
+ await page.waitForSelector('#newPassword input', { state: 'visible', timeout: 20000 })
41
45
 
42
46
  const firstPassword =
43
47
  passwordGenerator.generate({ length: 10, numbers: true }) + (Math.random() * 10).toFixed()
44
- await page.locator('input[type="password"]').nth(0).fill(firstPassword)
45
- await page.locator('input[type="password"]').nth(1).fill(firstPassword)
46
- await page.click('button[type=submit]')
47
- assert.ok(page.url().includes('/user/settings/change-password-finished'))
48
+ await setPrimePasswordFieldValue(page, '#newPassword', firstPassword)
49
+ await setPrimePasswordFieldValue(page, '#reenterPassword', firstPassword)
50
+ await Promise.all([
51
+ page.waitForURL((u) => u.pathname.includes('/user/settings/change-password-finished'), { timeout: 20000 }),
52
+ page.locator('button[type="submit"]').click()
53
+ ])
48
54
 
49
55
  await new Promise((r) => setTimeout(r, 200))
50
56
  const firstPasswordAuthenticationData = await PasswordAuthentication.get(user)
51
57
  assert.ok(firstPasswordAuthenticationData, 'password set')
52
58
 
53
59
  await page.goto(env.url + '/user/settings/change-password', { waitUntil: 'networkidle' })
54
- await page.getByText('Change password').waitFor({ state: 'visible' })
60
+ await waitForHydration(page)
61
+ await page.waitForSelector('#currentPassword input', { state: 'visible', timeout: 20000 })
55
62
 
56
63
  const secondPassword =
57
64
  passwordGenerator.generate({ length: 10, numbers: true }) + (Math.random() * 10).toFixed()
58
- await page.locator('input[type="password"]').nth(0).fill(firstPassword)
59
- await page.locator('input[type="password"]').nth(1).fill(secondPassword)
60
- await page.locator('input[type="password"]').nth(2).fill(secondPassword)
61
- await page.click('button[type=submit]')
65
+ await setPrimePasswordFieldValue(page, '#currentPassword', firstPassword)
66
+ await setPrimePasswordFieldValue(page, '#newPassword', secondPassword)
67
+ await setPrimePasswordFieldValue(page, '#reenterPassword', secondPassword)
68
+ await page.locator('button[type="submit"]').click()
62
69
 
63
70
  await new Promise((r) => setTimeout(r, 200))
64
71
  const secondPasswordAuthenticationData = await PasswordAuthentication.get(user)
@@ -1,10 +1,10 @@
1
- import test from 'node:test'
1
+ import { e2eSuite, test } from './e2eSuite.js'
2
+ import { waitForHydration } from '@live-change/e2e-test'
2
3
  import assert from 'node:assert'
3
4
  import App from '@live-change/framework'
4
5
  import randomProfile from 'random-profile-generator'
5
6
  import { withBrowser } from './withBrowser.js'
6
7
  import { useSecretCode } from './steps.js'
7
- import { e2eSuite } from './e2eSuite.js'
8
8
 
9
9
  const app = App.app()
10
10
  const randomUserData = randomProfile.profile()
@@ -25,6 +25,7 @@ e2eSuite('signInEmailCode', () => {
25
25
  await Email.create({ id: email, email, user })
26
26
 
27
27
  await page.goto(env.url + '/user/sign-in-email', { waitUntil: 'networkidle' })
28
+ await waitForHydration(page)
28
29
  await page.fill('input#email', email)
29
30
  await page.click('button[type=submit]')
30
31
  await page.waitForURL('**/sent/*', { timeout: 10000 })
@@ -54,6 +55,7 @@ e2eSuite('signInEmailCode', () => {
54
55
 
55
56
  if (!happyPath) {
56
57
  await page.goto(url, { waitUntil: 'networkidle' })
58
+ await waitForHydration(page)
57
59
  assert.ok(page.url().includes('/user/sign-in-finished'))
58
60
  }
59
61
  })
@@ -1,10 +1,10 @@
1
- import test from 'node:test'
1
+ import { e2eSuite, test } from './e2eSuite.js'
2
+ import { waitForHydration } from '@live-change/e2e-test'
2
3
  import assert from 'node:assert'
3
4
  import App from '@live-change/framework'
4
5
  import randomProfile from 'random-profile-generator'
5
6
  import { withBrowser } from './withBrowser.js'
6
7
  import { useSecretLink } from './steps.js'
7
- import { e2eSuite } from './e2eSuite.js'
8
8
 
9
9
  const app = App.app()
10
10
  const randomUserData = randomProfile.profile()
@@ -25,6 +25,7 @@ e2eSuite('signInEmailLink', () => {
25
25
  await Email.create({ id: email, email, user })
26
26
 
27
27
  await page.goto(env.url + '/user/sign-in-email', { waitUntil: 'networkidle' })
28
+ await waitForHydration(page)
28
29
  await page.fill('input#email', email)
29
30
  await page.click('button[type=submit]')
30
31
  await page.waitForURL('**/sent/*', { timeout: 10000 })
@@ -38,7 +39,7 @@ e2eSuite('signInEmailLink', () => {
38
39
  assert.strictEqual((authenticationData as { messageData?: { user: string } })?.messageData?.user, user, 'authentication contains user')
39
40
 
40
41
  const linkData = await useSecretLink(page, env, authentication, happyPath)
41
- await page.waitForURL('**/sign-in-finished', { timeout: 10000 })
42
+ await page.waitForURL('**/sign-in-finished', { timeout: 30000 })
42
43
 
43
44
  assert.ok(page.url().includes('/user/sign-in-finished'))
44
45
  const clientSession = await page.evaluate(
@@ -54,6 +55,7 @@ e2eSuite('signInEmailLink', () => {
54
55
 
55
56
  if (!happyPath) {
56
57
  await page.goto(env.url + '/user/link/' + linkData.secretCode, { waitUntil: 'networkidle' })
58
+ await waitForHydration(page)
57
59
  await page.getByText('Link used').waitFor({ state: 'visible' })
58
60
  }
59
61
  })