@live-change/user-frontend 0.9.204 → 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.
package/.node-version ADDED
@@ -0,0 +1 @@
1
+ 20.20.2
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ 20.20.2
@@ -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
  })
@@ -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 crypto from 'crypto'
6
7
  import passwordGenerator from 'generate-password'
7
8
  import { withBrowser } from './withBrowser.js'
8
- import { e2eSuite } from './e2eSuite.js'
9
9
 
10
10
  const app = App.app()
11
11
  const randomUserData = randomProfile.profile()
@@ -30,6 +30,7 @@ e2eSuite('signInEmailPassword', () => {
30
30
  await PasswordAuthentication.create({ id: user, user, passwordHash })
31
31
 
32
32
  await page.goto(env.url + '/user/sign-in-email', { waitUntil: 'networkidle' })
33
+ await waitForHydration(page)
33
34
  await page.fill('input#email', email)
34
35
  await page.fill('input[type="password"]', password)
35
36
  await page.click('button[type=submit]')
@@ -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()
@@ -20,18 +20,21 @@ e2eSuite('signOut', () => {
20
20
  await User.create({ id: user, roles: [] })
21
21
  await Email.create({ id: email, email, user })
22
22
  await page.goto(env.url + '/', { waitUntil: 'networkidle' })
23
+ await waitForHydration(page)
23
24
  const session = await page.evaluate(
24
25
  () => (window as unknown as { api: { client: { value: { session: string } } } }).api.client.value.session
25
26
  )
26
27
  await AuthenticatedUser.create({ id: session, user, session })
27
28
 
28
29
  await page.reload({ waitUntil: 'networkidle' })
30
+ await waitForHydration(page)
29
31
  const clientUser = await page.evaluate(
30
32
  () => (window as unknown as { api: { client: { value: { user: string } } } }).api.client.value.user
31
33
  )
32
34
  assert.strictEqual(user, clientUser, 'client logged in')
33
35
 
34
36
  await page.goto(env.url + '/user/sign-out', { waitUntil: 'networkidle' })
37
+ await waitForHydration(page)
35
38
  await page.waitForURL('**/sign-out-finished', { timeout: 10000 })
36
39
  assert.ok(page.url().includes('/user/sign-out-finished'))
37
40
 
@@ -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 randomProfile from 'random-profile-generator'
4
5
  import { withBrowser } from './withBrowser.js'
5
6
  import { useSecretCode } from './steps.js'
6
- import { e2eSuite } from './e2eSuite.js'
7
7
 
8
8
  const user = randomProfile.profile()
9
9
  ;(user as { email?: string }).email =
@@ -14,6 +14,7 @@ e2eSuite('signUpEmailCode', () => {
14
14
  test('sign up with email code', async () => {
15
15
  await withBrowser(async (page, env) => {
16
16
  await page.goto(env.url + '/user/sign-up-email', { waitUntil: 'networkidle' })
17
+ await waitForHydration(page)
17
18
  await page.fill('input#email', (user as { email: string }).email)
18
19
  await page.click('button[type=submit]')
19
20
  await page.waitForURL('**/sent/*', { timeout: 10000 })
@@ -42,6 +43,8 @@ e2eSuite('signUpEmailCode', () => {
42
43
 
43
44
  if (!happyPath) {
44
45
  await page.goto(url, { waitUntil: 'networkidle' })
46
+ await waitForHydration(page)
47
+ await page.waitForURL('**/sign-up-finished', { timeout: 15000 })
45
48
  assert.ok(page.url().includes('/user/sign-up-finished'))
46
49
  }
47
50
  })
@@ -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 randomProfile from 'random-profile-generator'
4
5
  import { withBrowser } from './withBrowser.js'
5
6
  import { useSecretLink } from './steps.js'
6
- import { e2eSuite } from './e2eSuite.js'
7
7
 
8
8
  const user = randomProfile.profile()
9
9
  ;(user as { email?: string }).email =
@@ -14,6 +14,7 @@ e2eSuite('signUpEmailLink', () => {
14
14
  test('sign up with email link', async () => {
15
15
  await withBrowser(async (page, env) => {
16
16
  await page.goto(env.url + '/user/sign-up-email', { waitUntil: 'networkidle' })
17
+ await waitForHydration(page)
17
18
  await page.fill('input#email', (user as { email: string }).email)
18
19
  await page.click('button[type=submit]')
19
20
  await page.waitForURL('**/sent/*', { timeout: 10000 })
@@ -26,7 +27,7 @@ e2eSuite('signUpEmailLink', () => {
26
27
  assert.ok(authenticationData, 'authentication created')
27
28
 
28
29
  const linkData = await useSecretLink(page, env, authentication, happyPath)
29
- await page.waitForURL('**/sign-up-finished', { timeout: 10000 })
30
+ await page.waitForURL('**/sign-up-finished', { timeout: 30000 })
30
31
 
31
32
  assert.ok(page.url().includes('/user/sign-up-finished'))
32
33
  const clientSession = await page.evaluate(
@@ -42,6 +43,7 @@ e2eSuite('signUpEmailLink', () => {
42
43
 
43
44
  if (!happyPath) {
44
45
  await page.goto(env.url + '/user/link/' + linkData.secretCode, { waitUntil: 'networkidle' })
46
+ await waitForHydration(page)
45
47
  await page.getByText('Link used').waitFor({ state: 'visible' })
46
48
  }
47
49
  })
package/e2e/steps.ts CHANGED
@@ -1,5 +1,14 @@
1
1
  import assert from 'node:assert'
2
2
  import type { Page } from 'playwright'
3
+ import { waitForHydration } from '@live-change/e2e-test'
4
+
5
+ /** PrimeVue Password: overlay blocks pointer clicks; set value + input so command-form validators see it. */
6
+ export async function setPrimePasswordFieldValue(page: Page, rootSelector: string, value: string) {
7
+ await page.locator(`${rootSelector} input`).evaluate((el: HTMLInputElement, v: string) => {
8
+ el.value = v
9
+ el.dispatchEvent(new Event('input', { bubbles: true }))
10
+ }, value)
11
+ }
3
12
  import App from '@live-change/framework'
4
13
  import randomProfile from 'random-profile-generator'
5
14
  import passwordGenerator from 'generate-password'
@@ -58,6 +67,7 @@ export async function useEmailLink(
58
67
  }
59
68
  const link = await Link.indexObjectGet('byAuthentication', authentication)
60
69
  await page.goto(env.url + prefix + link.secretCode, { waitUntil: 'networkidle' })
70
+ await waitForHydration(page)
61
71
  await new Promise((r) => setTimeout(r, 100))
62
72
  return { authentication, link }
63
73
  }
@@ -74,6 +84,11 @@ export async function amLoggedOut(page: Page, env: TestEnv): Promise<void> {
74
84
  await AuthenticatedUser.delete(session)
75
85
  }
76
86
 
87
+ function newestByExpire<T extends { expire: Date }>(rows: T[]): T | undefined {
88
+ if (!rows?.length) return undefined
89
+ return [...rows].sort((a, b) => new Date(b.expire).getTime() - new Date(a.expire).getTime())[0]
90
+ }
91
+
77
92
  export async function useSecretCode(
78
93
  page: Page,
79
94
  env: TestEnv,
@@ -81,11 +96,11 @@ export async function useSecretCode(
81
96
  happyPath: boolean
82
97
  ): Promise<void> {
83
98
  const Code = env.haveModel('secretCode', 'Code') as {
84
- indexObjectGet: (index: string, key: unknown) => Promise<{ id: string; secretCode: string; expire: Date }>
85
99
  indexRangeGet: (index: string, key: unknown) => Promise<{ id: string; secretCode: string; expire: Date }[]>
86
100
  update: (id: string, data: { expire: Date }) => Promise<unknown>
87
101
  }
88
- let codeData = await Code.indexObjectGet('byAuthentication', authentication)
102
+ const initialRows = await Code.indexRangeGet('byAuthentication', authentication)
103
+ let codeData = newestByExpire(initialRows || [])
89
104
  assert.ok(codeData, 'code created')
90
105
 
91
106
  if (!happyPath) {
@@ -108,26 +123,42 @@ export async function useSecretCode(
108
123
  await page.click('button[type=submit]')
109
124
  await page.getByRole('alert').waitFor({ state: 'visible' })
110
125
 
111
- await page.click('text=Resend')
126
+ await page.getByTestId('message-auth-resend-code').click()
112
127
  assert.ok(page.url().includes('/sent/'))
113
-
114
- await new Promise((r) => setTimeout(r, 200))
115
- const newCodeData = await Code.indexRangeGet('byAuthentication', authentication)
116
- newCodeData.sort((a, b) => new Date(b.expire).getTime() - new Date(a.expire).getTime())
128
+ // Resend runs async (workingZone); wait for toast so form.reset() and router.push complete before polling/submit.
129
+ await page.getByText(/New code sent to you|Nowy kod został|Code sent|Kod wysłany/i).first()
130
+ .waitFor({ state: 'visible', timeout: 20000 })
131
+ await sleep(300)
132
+
133
+ // resendMessageAuthentication keeps the same authentication id; new Code row appears asynchronously.
134
+ const oldCodeId = codeData!.id
135
+ let newCodeData: { id: string; secretCode: string; expire: Date }[] = []
136
+ const deadline = Date.now() + 12000
137
+ while (Date.now() < deadline) {
138
+ await new Promise((r) => setTimeout(r, 250))
139
+ newCodeData = await Code.indexRangeGet('byAuthentication', authentication)
140
+ const newest = newestByExpire(newCodeData)
141
+ if (newest && newest.id !== oldCodeId) {
142
+ newCodeData = [newest]
143
+ break
144
+ }
145
+ }
117
146
  const oldCodeData = codeData
118
- codeData = newCodeData[0]
119
- assert.ok(codeData, 'code exists')
147
+ codeData = newestByExpire(newCodeData) || newCodeData[0]
148
+ assert.ok(codeData, 'code exists after resend')
120
149
  assert.notStrictEqual(oldCodeData!.id, codeData!.id, 'code is different from previous code')
121
150
  }
122
151
 
123
- await page.waitForFunction(() => {
124
- const input = document.querySelector('input#code') as HTMLInputElement
125
- if(!input) return false
126
- return input.value === ''
127
- })
128
- await page.fill('input#code', codeData!.secretCode)
129
- await page.fill('input#code', codeData!.secretCode)
130
- await page.click('button[type=submit]')
152
+ const codeInput = page.locator('input#code')
153
+ await codeInput.waitFor({ state: 'visible', timeout: 15000 })
154
+ await sleep(200)
155
+ const finalSecret = codeData!.secretCode
156
+ await codeInput.evaluate((el: HTMLInputElement, v: string) => {
157
+ el.value = v
158
+ el.dispatchEvent(new Event('input', { bubbles: true }))
159
+ }, finalSecret)
160
+ await page.locator('input#code').click()
161
+ await page.getByRole('button', { name: /^ok$/i }).click({ force: true })
131
162
  await new Promise((r) => setTimeout(r, 100))
132
163
  }
133
164
 
@@ -139,25 +170,30 @@ export async function useSecretLink(
139
170
  prefix = '/user'
140
171
  ): Promise<{ secretCode: string }> {
141
172
  const Link = env.haveModel('secretLink', 'Link') as {
142
- indexObjectGet: (index: string, key: unknown) => Promise<{ id: string; secretCode: string; expire: Date }>
143
173
  indexRangeGet: (index: string, key: unknown) => Promise<{ id: string; secretCode: string; expire: Date }[]>
144
174
  update: (id: string, data: { expire: Date }) => Promise<unknown>
145
175
  }
146
- let linkData = await Link.indexObjectGet('byAuthentication', authentication)
176
+ const linkRows = await Link.indexRangeGet('byAuthentication', authentication)
177
+ let linkData = newestByExpire(linkRows || [])
147
178
  assert.ok(linkData, 'link created')
148
179
 
149
180
  if (!happyPath) {
150
181
  await page.goto(env.url + prefix + '/link/[badSecret]', { waitUntil: 'networkidle' })
182
+ await waitForHydration(page)
151
183
  await page.getByText('Unknown link').waitFor({ state: 'visible' })
152
184
  }
153
185
 
154
186
  if (!happyPath) {
155
187
  await new Promise((r) => setTimeout(r, 200))
156
- await Link.update(linkData!.id, { expire: new Date() })
188
+ await Link.update(linkData!.id, { expire: new Date(Date.now() - 1000) })
157
189
  await page.goto(env.url + prefix + '/link/' + linkData!.secretCode, { waitUntil: 'networkidle' })
190
+ await waitForHydration(page)
158
191
  await page.getByText('Link expired').waitFor({ state: 'visible' })
159
192
 
160
- await page.click('text=Resend')
193
+ await page.getByTestId('message-auth-resend-link').click()
194
+
195
+ await page.waitForURL('**/sent/*', { timeout: 10000 })
196
+
161
197
  assert.ok(page.url().includes(prefix + '/sent/'))
162
198
 
163
199
  await new Promise((r) => setTimeout(r, 200))
@@ -1,19 +1,5 @@
1
- import { chromium } from 'playwright'
1
+ import { createWithBrowser } from '@live-change/e2e-test'
2
2
  import { getTestEnv } from './env.js'
3
3
  import type { TestEnv } from './env.js'
4
- import type { Page } from 'playwright'
5
4
 
6
- export async function withBrowser(
7
- fn: (page: Page, env: TestEnv) => Promise<void>
8
- ): Promise<void> {
9
- const env = await getTestEnv()
10
- const browser = await chromium.launch({ headless: process.env.SHOW_BROWSER ? false : true })
11
- const context = await browser.newContext()
12
- const page = await context.newPage()
13
- try {
14
- await fn(page, env)
15
- } finally {
16
- await context.close()
17
- await browser.close()
18
- }
19
- }
5
+ export const withBrowser = createWithBrowser<TestEnv>(getTestEnv)
@@ -40,7 +40,7 @@
40
40
 
41
41
  <NotificationsIcon />
42
42
 
43
- <UserIcon />
43
+ <UserIcon :menuStyle="{ right: '5px' }" />
44
44
 
45
45
  <a v-ripple class="cursor-pointer block lg:hidden text-surface-700 dark:text-surface-100 p-ripple ml-2 hover:bg-surface-100 dark:hover:bg-surface-700 p-2"
46
46
  v-styleclass="{ selector: '.top-menu', enterFromClass: 'hidden', leaveToClass: 'hidden', hideOnOutsideClick: true }">
@@ -38,7 +38,8 @@
38
38
 
39
39
  <div class="flex flex-row flex-wrap">
40
40
  <router-link v-for="contactType in contactsTypes"
41
- :to="{ name: 'user:connect-'+contactType.contactType }" class="mr-2 no-underline block mb-1">
41
+ :to="{ name: 'user:connect-'+contactType.contactType }" class="mr-2 no-underline block mb-1"
42
+ :data-testid="contactType.contactType === 'email' ? 'connect-email' : null">
42
43
  <Button v-if="contactType.contactType === 'email'"
43
44
  :label="t('connected.addEmail')" icon="pi pi-envelope" id="connect" />
44
45
  <Button v-else-if="contactType.contactType === 'phone'"
@@ -20,7 +20,7 @@
20
20
  <p class="mt-0 mb-6 p-0 leading-normal">
21
21
  {{ t('messageAuth.linkExpiredDesc') }}
22
22
  </p>
23
- <Button :label="t('messageAuth.resend')" class="p-button-lg" @click="resend"></Button>
23
+ <Button :label="t('messageAuth.resend')" class="p-button-lg" data-testid="message-auth-resend-link" @click="resend"></Button>
24
24
  </div>
25
25
 
26
26
  <div v-if="isReady || isRedirecting"
@@ -83,7 +83,10 @@
83
83
  const authenticationState = computed(() => link?.value?.authenticationData?.state)
84
84
 
85
85
  const isUnknown = computed(() => link.value === null)
86
- const isExpired = computed(() => link.value ? (now.value.toISOString() > link.value.expire) : false )
86
+ const isExpired = computed(() => {
87
+ if (!link.value?.expire) return false
88
+ return now.value.getTime() > new Date(link.value.expire).getTime()
89
+ })
87
90
  const isUsed = computed(() => authenticationState.value && authenticationState.value === 'used')
88
91
  const isReady = computed(() => !(isUnknown.value || isExpired.value || isUsed.value))
89
92
 
@@ -29,13 +29,12 @@
29
29
  </Message>
30
30
  </div>
31
31
  <div class="flex flex-col">
32
- <Button :label="t('common.ok')" type="submit" class="p-button-lg grow-0"
33
- :disableda="data.secret?.length < 6" />
32
+ <Button :label="t('common.ok')" type="submit" class="p-button-lg grow-0" />
34
33
  </div>
35
34
  </div>
36
35
  <div v-if="data.secretError === 'codeExpired'" class="mt-4 text-center">
37
36
  <p class="mt-0 mb-2 p-0 leading-normal">{{ t('messageAuth.toSendAnotherCode') }}</p>
38
- <Button :label="t('messageAuth.resendSecretCode')" class="p-button-lg" @click="resend" />
37
+ <Button :label="t('messageAuth.resendSecretCode')" class="p-button-lg" data-testid="message-auth-resend-code" @click="resend" />
39
38
  </div>
40
39
  </command-form>
41
40
  </Secured>
@@ -51,11 +51,7 @@
51
51
  <script setup>
52
52
  import Button from "primevue/button"
53
53
 
54
- const { action, contact, json } = defineProps({
55
- action: {
56
- type: String,
57
- required: true
58
- },
54
+ const { contact, json } = defineProps({
59
55
  contact: {
60
56
  type: String,
61
57
  required: true
@@ -51,11 +51,7 @@
51
51
  <script setup>
52
52
  import Button from "primevue/button"
53
53
 
54
- const { action, contact, json } = defineProps({
55
- action: {
56
- type: String,
57
- required: true
58
- },
54
+ const { contact, json } = defineProps({
59
55
  contact: {
60
56
  type: String,
61
57
  required: true
@@ -51,11 +51,7 @@
51
51
  <script setup>
52
52
  import Button from "primevue/button"
53
53
 
54
- const { action, contact, json } = defineProps({
55
- action: {
56
- type: String,
57
- required: true
58
- },
54
+ const { contact, json } = defineProps({
59
55
  contact: {
60
56
  type: String,
61
57
  required: true
@@ -9,11 +9,7 @@
9
9
  </template>
10
10
 
11
11
  <script setup>
12
- const { action, contact, json } = defineProps({
13
- action: {
14
- type: String,
15
- required: true
16
- },
12
+ const { contact, json } = defineProps({
17
13
  contact: {
18
14
  type: String,
19
15
  required: true
@@ -9,11 +9,7 @@
9
9
  </template>
10
10
 
11
11
  <script setup>
12
- const { action, contact, json } = defineProps({
13
- action: {
14
- type: String,
15
- required: true
16
- },
12
+ const { contact, json } = defineProps({
17
13
  contact: {
18
14
  type: String,
19
15
  required: true
@@ -9,11 +9,7 @@
9
9
  </template>
10
10
 
11
11
  <script setup>
12
- const { action, contact, json } = defineProps({
13
- action: {
14
- type: String,
15
- required: true
16
- },
12
+ const { contact, json } = defineProps({
17
13
  contact: {
18
14
  type: String,
19
15
  required: true
@@ -115,7 +115,9 @@
115
115
 
116
116
  const isUnknown = computed(() => authentication.value === null)
117
117
  const isExpired = computed(() =>
118
- authentication.value ? (now.value.toISOString() > authentication.value.expire) : false )
118
+ authentication.value?.expire
119
+ ? now.value.getTime() > new Date(authentication.value.expire).getTime()
120
+ : false)
119
121
  const isUsed = computed(() => !working.value && !redirecting.value && authentication.value && authentication.value.state === 'used')
120
122
  const isReady = computed(() => !(isUnknown.value || isExpired.value || isUsed.value))
121
123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/user-frontend",
3
- "version": "0.9.204",
3
+ "version": "0.9.205",
4
4
  "scripts": {
5
5
  "memDev": "tsx --inspect --expose-gc server/start.js memDev --enableSessions --initScript ./init.js --dbAccess",
6
6
  "localDevInit": "tsx server/start.js localDev --enableSessions --initScript ./init.js --dbAccess",
@@ -33,33 +33,37 @@
33
33
  "debug": "tsx --inspect-brk server",
34
34
  "describe": "tsx server/start.js describe",
35
35
  "changes": "tsx server/start.js changes",
36
- "e2e": "node --import tsx --test --test-concurrency=1 e2e/*.test.ts"
36
+ "e2e": "fnm exec -- node --import tsx e2e/runner.ts",
37
+ "e2e:time": "fnm exec -- node --import tsx e2e/execution-time-report.ts",
38
+ "e2e:time:log": "bash -c 'set -o pipefail && fnm exec -- node --import tsx e2e/execution-time-report.ts 2>&1 | tee e2e/last-e2e-time-debug.log'",
39
+ "e2e:file": "fnm exec -- node --import tsx e2e/runner.ts"
37
40
  },
38
41
  "type": "module",
39
42
  "dependencies": {
40
- "@live-change/cli": "^0.9.204",
41
- "@live-change/dao": "^0.9.204",
42
- "@live-change/dao-vue3": "^0.9.204",
43
- "@live-change/dao-websocket": "^0.9.204",
44
- "@live-change/email-service": "^0.9.204",
45
- "@live-change/framework": "^0.9.204",
46
- "@live-change/identicon-service": "^0.9.204",
47
- "@live-change/image-frontend": "^0.9.204",
48
- "@live-change/message-authentication-service": "^0.9.204",
49
- "@live-change/notification-service": "^0.9.204",
50
- "@live-change/password-authentication-service": "^0.9.204",
51
- "@live-change/pattern": "^0.9.204",
52
- "@live-change/secret-code-service": "^0.9.204",
53
- "@live-change/secret-link-service": "^0.9.204",
54
- "@live-change/security-frontend": "^0.9.204",
55
- "@live-change/security-service": "^0.9.204",
56
- "@live-change/session-service": "^0.9.204",
57
- "@live-change/timer-service": "^0.9.204",
58
- "@live-change/upload-service": "^0.9.204",
59
- "@live-change/user-identification-service": "^0.9.204",
60
- "@live-change/user-service": "^0.9.204",
61
- "@live-change/vue3-components": "^0.9.204",
62
- "@live-change/vue3-ssr": "^0.9.204",
43
+ "@live-change/cli": "^0.9.205",
44
+ "@live-change/dao": "^0.9.205",
45
+ "@live-change/dao-vue3": "^0.9.205",
46
+ "@live-change/dao-websocket": "^0.9.205",
47
+ "@live-change/e2e-test": "^0.9.205",
48
+ "@live-change/email-service": "^0.9.205",
49
+ "@live-change/framework": "^0.9.205",
50
+ "@live-change/identicon-service": "^0.9.205",
51
+ "@live-change/image-frontend": "^0.9.205",
52
+ "@live-change/message-authentication-service": "^0.9.205",
53
+ "@live-change/notification-service": "^0.9.205",
54
+ "@live-change/password-authentication-service": "^0.9.205",
55
+ "@live-change/pattern": "^0.9.205",
56
+ "@live-change/secret-code-service": "^0.9.205",
57
+ "@live-change/secret-link-service": "^0.9.205",
58
+ "@live-change/security-frontend": "^0.9.205",
59
+ "@live-change/security-service": "^0.9.205",
60
+ "@live-change/session-service": "^0.9.205",
61
+ "@live-change/timer-service": "^0.9.205",
62
+ "@live-change/upload-service": "^0.9.205",
63
+ "@live-change/user-identification-service": "^0.9.205",
64
+ "@live-change/user-service": "^0.9.205",
65
+ "@live-change/vue3-components": "^0.9.205",
66
+ "@live-change/vue3-ssr": "^0.9.205",
63
67
  "@vueuse/core": "^12.3.0",
64
68
  "codeceptjs-assert": "^0.0.5",
65
69
  "codeceptjs-video-helper": "0.1.3",
@@ -80,16 +84,20 @@
80
84
  "wtfnode": "^0.9.1"
81
85
  },
82
86
  "devDependencies": {
83
- "@live-change/codeceptjs-helper": "^0.9.204",
87
+ "@live-change/codeceptjs-helper": "^0.9.205",
84
88
  "codeceptjs": "^3.7.6",
85
- "generate-password": "1.7.1",
86
- "playwright": "1.49.1",
89
+ "generate-password": "^1.7.1",
90
+ "playwright": "^1.49.1",
87
91
  "random-profile-generator": "^2.3.0",
92
+ "tsx": "^4.21.0",
88
93
  "txtgen": "^3.0.7",
89
94
  "webdriverio": "^9.5.1"
90
95
  },
91
96
  "author": "Michał Łaszczewski <michal@laszczewski.pl>",
92
97
  "license": "BSD-3-Clause",
93
98
  "description": "",
94
- "gitHead": "6ac17ccf3c184ca4d7ef920f63827dbfd8749f96"
99
+ "gitHead": "ef195e51ea283e56d891b11da5d5f586691507db",
100
+ "engines": {
101
+ "node": "20.20.2"
102
+ }
95
103
  }