@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 +1 -0
- package/.nvmrc +1 -0
- package/e2e/connectEmailCode.test.ts +11 -7
- package/e2e/connectEmailLink.test.ts +12 -6
- package/e2e/delete.test.ts +6 -3
- package/e2e/disconnectEmail.test.ts +11 -12
- package/e2e/e2eSuite.ts +8 -12
- package/e2e/env.ts +56 -58
- package/e2e/execution-time-report.ts +7 -0
- package/e2e/resetPasswordWithEmailCode.test.ts +8 -6
- package/e2e/resetPasswordWithEmailLink.test.ts +8 -6
- package/e2e/runner.ts +10 -0
- package/e2e/setPassword.test.ts +19 -12
- package/e2e/signInEmailCode.test.ts +4 -2
- package/e2e/signInEmailLink.test.ts +5 -3
- package/e2e/signInEmailPassword.test.ts +3 -2
- package/e2e/signOut.test.ts +5 -2
- package/e2e/signUpEmailCode.test.ts +5 -2
- package/e2e/signUpEmailLink.test.ts +5 -3
- package/e2e/steps.ts +57 -21
- package/e2e/withBrowser.ts +2 -16
- package/front/src/NavBar.vue +1 -1
- package/front/src/connected/Connected.vue +2 -1
- package/front/src/message-auth/MessageLink.vue +5 -2
- package/front/src/message-auth/MessageSent.vue +2 -3
- package/front/src/message-auth/email/ConnectEmail.vue +1 -5
- package/front/src/message-auth/email/ResetPasswordEmail.vue +1 -5
- package/front/src/message-auth/email/SignInEmail.vue +1 -5
- package/front/src/message-auth/sms/ResetPasswordSms.vue +1 -5
- package/front/src/message-auth/sms/SignInSms.vue +1 -5
- package/front/src/message-auth/sms/SignUpSms.vue +1 -5
- package/front/src/password/ResetPasswordForm.vue +3 -1
- package/package.json +37 -29
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 '
|
|
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
|
|
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.
|
|
38
|
-
|
|
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('
|
|
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:
|
|
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 '
|
|
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.
|
|
37
|
-
|
|
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:
|
|
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
|
})
|
package/e2e/delete.test.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import test from '
|
|
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
|
|
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 '
|
|
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
|
|
35
|
-
await page.getByText(
|
|
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.
|
|
38
|
-
await page.
|
|
39
|
-
assert.strictEqual(await page.getByText(
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
8
|
-
const
|
|
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 =
|
|
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) => {
|
|
38
|
-
|
|
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
|
|
57
|
+
const s = testServer
|
|
58
|
+
if (!s) return
|
|
52
59
|
testServer = null
|
|
53
60
|
envPromise = null
|
|
54
|
-
|
|
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:
|
|
103
|
-
haveModel:
|
|
104
|
-
haveView:
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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:
|
|
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
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import test from '
|
|
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
|
|
57
|
-
await page
|
|
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 '
|
|
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
|
|
57
|
-
await page
|
|
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))
|
package/e2e/setPassword.test.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import test from '
|
|
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
|
|
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
|
|
45
|
-
await page
|
|
46
|
-
await
|
|
47
|
-
|
|
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
|
|
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
|
|
59
|
-
await page
|
|
60
|
-
await page
|
|
61
|
-
await page.
|
|
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 '
|
|
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 '
|
|
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:
|
|
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 '
|
|
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]')
|
package/e2e/signOut.test.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import test from '
|
|
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 '
|
|
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 '
|
|
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:
|
|
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
|
-
|
|
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.
|
|
126
|
+
await page.getByTestId('message-auth-resend-code').click()
|
|
112
127
|
assert.ok(page.url().includes('/sent/'))
|
|
113
|
-
|
|
114
|
-
await
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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.
|
|
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))
|
package/e2e/withBrowser.ts
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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)
|
package/front/src/NavBar.vue
CHANGED
|
@@ -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(() =>
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
@@ -115,7 +115,9 @@
|
|
|
115
115
|
|
|
116
116
|
const isUnknown = computed(() => authentication.value === null)
|
|
117
117
|
const isExpired = computed(() =>
|
|
118
|
-
|
|
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.
|
|
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
|
|
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.
|
|
41
|
-
"@live-change/dao": "^0.9.
|
|
42
|
-
"@live-change/dao-vue3": "^0.9.
|
|
43
|
-
"@live-change/dao-websocket": "^0.9.
|
|
44
|
-
"@live-change/
|
|
45
|
-
"@live-change/
|
|
46
|
-
"@live-change/
|
|
47
|
-
"@live-change/
|
|
48
|
-
"@live-change/
|
|
49
|
-
"@live-change/
|
|
50
|
-
"@live-change/
|
|
51
|
-
"@live-change/
|
|
52
|
-
"@live-change/
|
|
53
|
-
"@live-change/secret-
|
|
54
|
-
"@live-change/
|
|
55
|
-
"@live-change/security-
|
|
56
|
-
"@live-change/
|
|
57
|
-
"@live-change/
|
|
58
|
-
"@live-change/
|
|
59
|
-
"@live-change/
|
|
60
|
-
"@live-change/user-service": "^0.9.
|
|
61
|
-
"@live-change/
|
|
62
|
-
"@live-change/vue3-
|
|
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.
|
|
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": "
|
|
99
|
+
"gitHead": "ef195e51ea283e56d891b11da5d5f586691507db",
|
|
100
|
+
"engines": {
|
|
101
|
+
"node": "20.20.2"
|
|
102
|
+
}
|
|
95
103
|
}
|