@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,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.203",
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.203",
41
- "@live-change/dao": "^0.9.203",
42
- "@live-change/dao-vue3": "^0.9.203",
43
- "@live-change/dao-websocket": "^0.9.203",
44
- "@live-change/email-service": "^0.9.203",
45
- "@live-change/framework": "^0.9.203",
46
- "@live-change/identicon-service": "^0.9.203",
47
- "@live-change/image-frontend": "^0.9.203",
48
- "@live-change/message-authentication-service": "^0.9.203",
49
- "@live-change/notification-service": "^0.9.203",
50
- "@live-change/password-authentication-service": "^0.9.203",
51
- "@live-change/pattern": "^0.9.203",
52
- "@live-change/secret-code-service": "^0.9.203",
53
- "@live-change/secret-link-service": "^0.9.203",
54
- "@live-change/security-frontend": "^0.9.203",
55
- "@live-change/security-service": "^0.9.203",
56
- "@live-change/session-service": "^0.9.203",
57
- "@live-change/timer-service": "^0.9.203",
58
- "@live-change/upload-service": "^0.9.203",
59
- "@live-change/user-identification-service": "^0.9.203",
60
- "@live-change/user-service": "^0.9.203",
61
- "@live-change/vue3-components": "^0.9.203",
62
- "@live-change/vue3-ssr": "^0.9.203",
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.203",
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": "4d4ce413fe747da2c398eb4ad378e37cc81874b3"
99
+ "gitHead": "ef195e51ea283e56d891b11da5d5f586691507db",
100
+ "engines": {
101
+ "node": "20.20.2"
102
+ }
95
103
  }