mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-04 15:39:45 +00:00
Co-authored-by: Kyle Mendell <kmendell@ofkm.us> Co-authored-by: Kyle Mendell <kmendell@outlook.com> Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
10665
frontend/package-lock.json
generated
10665
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,60 +1,62 @@
|
|||||||
{
|
{
|
||||||
"name": "pocket-id-frontend",
|
"name": "pocket-id-frontend",
|
||||||
"version": "0.46.0",
|
"version": "0.46.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --port 3000",
|
"dev": "vite dev --port 3000",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview --port 3000",
|
"preview": "vite preview --port 3000",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"lint": "prettier --check . && eslint .",
|
"lint": "prettier --check . && eslint .",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simplewebauthn/browser": "^13.1.0",
|
"@simplewebauthn/browser": "^13.1.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"axios": "^1.8.2",
|
"axios": "^1.8.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"formsnap": "^1.0.1",
|
"formsnap": "^1.0.1",
|
||||||
"jose": "^5.9.6",
|
"jose": "^5.9.6",
|
||||||
"lucide-svelte": "^0.487.0",
|
"lucide-svelte": "^0.487.0",
|
||||||
"mode-watcher": "^0.5.1",
|
"mode-watcher": "^0.5.1",
|
||||||
"svelte-sonner": "^0.3.28",
|
"qrcode": "^1.5.4",
|
||||||
"sveltekit-superforms": "^2.23.1",
|
"svelte-sonner": "^0.3.28",
|
||||||
"tailwind-merge": "^2.6.0",
|
"sveltekit-superforms": "^2.23.1",
|
||||||
"tailwind-variants": "^0.3.1",
|
"tailwind-merge": "^2.6.0",
|
||||||
"zod": "^3.24.1"
|
"tailwind-variants": "^0.3.1",
|
||||||
},
|
"zod": "^3.24.1"
|
||||||
"devDependencies": {
|
},
|
||||||
"@inlang/paraglide-js": "^2.0.0",
|
"devDependencies": {
|
||||||
"@inlang/plugin-m-function-matcher": "^2.0.7",
|
"@inlang/paraglide-js": "^2.0.0",
|
||||||
"@inlang/plugin-message-format": "^4.0.0",
|
"@inlang/plugin-m-function-matcher": "^2.0.7",
|
||||||
"@internationalized/date": "^3.7.0",
|
"@inlang/plugin-message-format": "^4.0.0",
|
||||||
"@playwright/test": "^1.50.0",
|
"@internationalized/date": "^3.7.0",
|
||||||
"@sveltejs/adapter-auto": "^4.0.0",
|
"@playwright/test": "^1.50.0",
|
||||||
"@sveltejs/adapter-node": "^5.2.12",
|
"@sveltejs/adapter-auto": "^4.0.0",
|
||||||
"@sveltejs/kit": "^2.16.1",
|
"@sveltejs/adapter-node": "^5.2.12",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
"@sveltejs/kit": "^2.16.1",
|
||||||
"@types/eslint": "^9.6.1",
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
"@types/node": "^22.10.10",
|
"@types/eslint": "^9.6.1",
|
||||||
"bits-ui": "^0.22.0",
|
"@types/node": "^22.10.10",
|
||||||
"cmdk-sv": "^0.0.19",
|
"@types/qrcode": "^1.5.5",
|
||||||
"eslint": "^9.19.0",
|
"bits-ui": "^0.22.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"cmdk-sv": "^0.0.19",
|
||||||
"eslint-plugin-svelte": "^2.46.1",
|
"eslint": "^9.19.0",
|
||||||
"globals": "^15.14.0",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"prettier": "^3.4.2",
|
"eslint-plugin-svelte": "^2.46.1",
|
||||||
"prettier-plugin-svelte": "^3.3.3",
|
"globals": "^15.14.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier": "^3.4.2",
|
||||||
"svelte": "^5.19.3",
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
"svelte-check": "^4.1.4",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"tailwindcss": "^4.0.0",
|
"svelte": "^5.19.3",
|
||||||
"tslib": "^2.8.1",
|
"svelte-check": "^4.1.4",
|
||||||
"typescript": "^5.7.3",
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript-eslint": "^8.21.0",
|
"tslib": "^2.8.1",
|
||||||
"vite": "^6.2.6"
|
"typescript": "^5.7.3",
|
||||||
}
|
"typescript-eslint": "^8.21.0",
|
||||||
|
"vite": "^6.2.6"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
|
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
|
||||||
|
import Qrcode from '$lib/components/qrcode/qrcode.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Dialog from '$lib/components/ui/dialog';
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
import Input from '$lib/components/ui/input/input.svelte';
|
|
||||||
import Label from '$lib/components/ui/label/label.svelte';
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
import * as Select from '$lib/components/ui/select/index.js';
|
import * as Select from '$lib/components/ui/select/index.js';
|
||||||
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
import UserService from '$lib/services/user-service';
|
import UserService from '$lib/services/user-service';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
|
import { mode } from 'mode-watcher';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
userId = $bindable()
|
userId = $bindable()
|
||||||
@@ -18,6 +21,7 @@
|
|||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
|
|
||||||
let oneTimeLink: string | null = $state(null);
|
let oneTimeLink: string | null = $state(null);
|
||||||
|
let code: string | null = $state(null);
|
||||||
let selectedExpiration: keyof typeof availableExpirations = $state(m.one_hour());
|
let selectedExpiration: keyof typeof availableExpirations = $state(m.one_hour());
|
||||||
|
|
||||||
let availableExpirations = {
|
let availableExpirations = {
|
||||||
@@ -31,8 +35,8 @@
|
|||||||
async function createOneTimeAccessToken() {
|
async function createOneTimeAccessToken() {
|
||||||
try {
|
try {
|
||||||
const expiration = new Date(Date.now() + availableExpirations[selectedExpiration] * 1000);
|
const expiration = new Date(Date.now() + availableExpirations[selectedExpiration] * 1000);
|
||||||
const token = await userService.createOneTimeAccessToken(expiration, userId!);
|
code = await userService.createOneTimeAccessToken(expiration, userId!);
|
||||||
oneTimeLink = `${page.url.origin}/lc/${token}`;
|
oneTimeLink = `${page.url.origin}/lc/${code}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
axiosErrorToast(e);
|
axiosErrorToast(e);
|
||||||
}
|
}
|
||||||
@@ -41,6 +45,7 @@
|
|||||||
function onOpenChange(open: boolean) {
|
function onOpenChange(open: boolean) {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
oneTimeLink = null;
|
oneTimeLink = null;
|
||||||
|
code = null;
|
||||||
userId = null;
|
userId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,6 +59,7 @@
|
|||||||
>{m.create_a_login_code_to_sign_in_without_a_passkey_once()}</Dialog.Description
|
>{m.create_a_login_code_to_sign_in_without_a_passkey_once()}</Dialog.Description
|
||||||
>
|
>
|
||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
|
|
||||||
{#if oneTimeLink === null}
|
{#if oneTimeLink === null}
|
||||||
<div>
|
<div>
|
||||||
<Label for="expiration">{m.expiration()}</Label>
|
<Label for="expiration">{m.expiration()}</Label>
|
||||||
@@ -65,7 +71,7 @@
|
|||||||
onSelectedChange={(v) =>
|
onSelectedChange={(v) =>
|
||||||
(selectedExpiration = v!.value as keyof typeof availableExpirations)}
|
(selectedExpiration = v!.value as keyof typeof availableExpirations)}
|
||||||
>
|
>
|
||||||
<Select.Trigger class="h-9 ">
|
<Select.Trigger class="h-9 w-full">
|
||||||
<Select.Value>{selectedExpiration}</Select.Value>
|
<Select.Value>{selectedExpiration}</Select.Value>
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
@@ -75,12 +81,36 @@
|
|||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
</div>
|
</div>
|
||||||
<Button onclick={() => createOneTimeAccessToken()} disabled={!selectedExpiration}>
|
<Button
|
||||||
|
onclick={() => createOneTimeAccessToken()}
|
||||||
|
disabled={!selectedExpiration}
|
||||||
|
class="mt-2 w-full"
|
||||||
|
>
|
||||||
{m.generate_code()}
|
{m.generate_code()}
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Label for="login-code" class="sr-only">{m.login_code()}</Label>
|
<div class="flex flex-col items-center gap-2">
|
||||||
<Input id="login-code" value={oneTimeLink} readonly />
|
<CopyToClipboard value={code!}>
|
||||||
|
<p class="text-3xl font-semibold">{code}</p>
|
||||||
|
</CopyToClipboard>
|
||||||
|
|
||||||
|
<div class="text-muted-foreground my-2 flex items-center justify-center gap-3">
|
||||||
|
<Separator />
|
||||||
|
<p class="text-nowrap text-xs">{m.or_visit()}</p>
|
||||||
|
<Separator />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Qrcode
|
||||||
|
class="mb-2"
|
||||||
|
value={oneTimeLink}
|
||||||
|
size={180}
|
||||||
|
color={$mode === 'dark' ? '#FFFFFF' : '#000000'}
|
||||||
|
backgroundColor={$mode === 'dark' ? '#000000' : '#FFFFFF'}
|
||||||
|
/>
|
||||||
|
<CopyToClipboard value={oneTimeLink!}>
|
||||||
|
<p data-testId="login-code-link">{oneTimeLink!}</p>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
|||||||
42
frontend/src/lib/components/qrcode/qrcode.svelte
Normal file
42
frontend/src/lib/components/qrcode/qrcode.svelte
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils/style';
|
||||||
|
import QRCode from 'qrcode';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let canvasEl: HTMLCanvasElement | null;
|
||||||
|
let {
|
||||||
|
value,
|
||||||
|
size = 200,
|
||||||
|
color = '#000000',
|
||||||
|
backgroundColor = '#FFFFFF',
|
||||||
|
...restProps
|
||||||
|
}: HTMLAttributes<HTMLCanvasElement> & {
|
||||||
|
value: string | null;
|
||||||
|
size?: number;
|
||||||
|
color?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (value && canvasEl) {
|
||||||
|
// Convert "transparent" to a valid value for the QR code library
|
||||||
|
const lightColor = backgroundColor === 'transparent' ? '#00000000' : backgroundColor;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
width: size,
|
||||||
|
margin: 0,
|
||||||
|
color: {
|
||||||
|
dark: color,
|
||||||
|
light: lightColor
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QRCode.toCanvas(canvasEl, value, options).catch((error: Error) => {
|
||||||
|
console.error('Error generating QR Code:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<canvas {...restProps} bind:this={canvasEl} class={cn('rounded-lg', restProps.class)}></canvas>
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
|
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
|
||||||
|
import Qrcode from '$lib/components/qrcode/qrcode.svelte';
|
||||||
import * as Dialog from '$lib/components/ui/dialog';
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
import UserService from '$lib/services/user-service';
|
import UserService from '$lib/services/user-service';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
|
import { mode } from 'mode-watcher';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
show = $bindable()
|
show = $bindable()
|
||||||
@@ -16,13 +18,17 @@
|
|||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
|
|
||||||
let code: string | null = $state(null);
|
let code: string | null = $state(null);
|
||||||
|
let loginCodeLink: string | null = $state(null);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (show) {
|
if (show) {
|
||||||
const expiration = new Date(Date.now() + 15 * 60 * 1000);
|
const expiration = new Date(Date.now() + 15 * 60 * 1000);
|
||||||
userService
|
userService
|
||||||
.createOneTimeAccessToken(expiration, 'me')
|
.createOneTimeAccessToken(expiration, 'me')
|
||||||
.then((c) => (code = c))
|
.then((c) => {
|
||||||
|
code = c;
|
||||||
|
loginCodeLink = page.url.origin + '/lc/' + code;
|
||||||
|
})
|
||||||
.catch((e) => axiosErrorToast(e));
|
.catch((e) => axiosErrorToast(e));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -48,16 +54,22 @@
|
|||||||
<CopyToClipboard value={code!}>
|
<CopyToClipboard value={code!}>
|
||||||
<p class="text-3xl font-semibold">{code}</p>
|
<p class="text-3xl font-semibold">{code}</p>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
<div class="text-muted-foreground flex items-center justify-center gap-3">
|
<div class="text-muted-foreground my-2 flex items-center justify-center gap-3">
|
||||||
<Separator />
|
<Separator />
|
||||||
<p class="text-nowrap text-xs">{m.or_visit()}</p>
|
<p class="text-nowrap text-xs">{m.or_visit()}</p>
|
||||||
<Separator />
|
<Separator />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<CopyToClipboard value={page.url.origin + '/lc/' + code!}>
|
<Qrcode
|
||||||
<p data-testId="login-code-link">{page.url.origin + '/lc/' + code!}</p>
|
class="mb-2"
|
||||||
</CopyToClipboard>
|
value={loginCodeLink}
|
||||||
</div>
|
size={180}
|
||||||
|
color={$mode === 'dark' ? '#FFFFFF' : '#000000'}
|
||||||
|
backgroundColor={$mode === 'dark' ? '#000000' : '#FFFFFF'}
|
||||||
|
/>
|
||||||
|
<CopyToClipboard value={loginCodeLink!}>
|
||||||
|
<p data-testId="login-code-link">{loginCodeLink!}</p>
|
||||||
|
</CopyToClipboard>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
|||||||
@@ -3,14 +3,13 @@
|
|||||||
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
|
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Table from '$lib/components/ui/table';
|
import * as Table from '$lib/components/ui/table';
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
import OIDCService from '$lib/services/oidc-service';
|
import OIDCService from '$lib/services/oidc-service';
|
||||||
import type { OidcClient } from '$lib/types/oidc.type';
|
import type { OidcClient } from '$lib/types/oidc.type';
|
||||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { LucidePencil, LucideTrash } from 'lucide-svelte';
|
import { LucidePencil, LucideTrash } from 'lucide-svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import OneTimeLinkModal from './client-secret.svelte';
|
|
||||||
import { m } from '$lib/paraglide/messages';
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
clients = $bindable(),
|
clients = $bindable(),
|
||||||
@@ -20,8 +19,6 @@
|
|||||||
requestOptions: SearchPaginationSortRequest;
|
requestOptions: SearchPaginationSortRequest;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let oneTimeLink = $state<string | null>(null);
|
|
||||||
|
|
||||||
const oidcService = new OIDCService();
|
const oidcService = new OIDCService();
|
||||||
|
|
||||||
async function deleteClient(client: OidcClient) {
|
async function deleteClient(client: OidcClient) {
|
||||||
@@ -86,5 +83,3 @@
|
|||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</AdvancedTable>
|
</AdvancedTable>
|
||||||
|
|
||||||
<OneTimeLinkModal {oneTimeLink} />
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import test, { expect } from '@playwright/test';
|
import test, { expect } from '@playwright/test';
|
||||||
import { users } from './data';
|
import { users } from './data';
|
||||||
|
import authUtil from './utils/auth.util';
|
||||||
import { cleanupBackend } from './utils/cleanup.util';
|
import { cleanupBackend } from './utils/cleanup.util';
|
||||||
import passkeyUtil from './utils/passkey.util';
|
import passkeyUtil from './utils/passkey.util';
|
||||||
import authUtil from './utils/auth.util';
|
|
||||||
|
|
||||||
test.beforeEach(cleanupBackend);
|
test.beforeEach(cleanupBackend);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { cleanupBackend } from './utils/cleanup.util';
|
|||||||
|
|
||||||
test.describe('API Key Management', () => {
|
test.describe('API Key Management', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await cleanupBackend()
|
await cleanupBackend();
|
||||||
await page.goto('/settings/admin/api-keys');
|
await page.goto('/settings/admin/api-keys');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -50,11 +50,13 @@ test('Create user fails with already taken username', async ({ page }) => {
|
|||||||
await expect(page.getByRole('status')).toHaveText('Username is already in use');
|
await expect(page.getByRole('status')).toHaveText('Username is already in use');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Create one time access token', async ({ page }) => {
|
test('Create one time access token', async ({ page, context }) => {
|
||||||
await page.goto('/settings/admin/users');
|
await page.goto('/settings/admin/users');
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('row', { name: `${users.craig.firstname} ${users.craig.lastname}` })
|
.getByRole('row', {
|
||||||
|
name: `${users.craig.firstname} ${users.craig.lastname}`
|
||||||
|
})
|
||||||
.getByRole('button')
|
.getByRole('button')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
@@ -64,16 +66,20 @@ test('Create one time access token', async ({ page }) => {
|
|||||||
await page.getByRole('option', { name: '12 hours' }).click();
|
await page.getByRole('option', { name: '12 hours' }).click();
|
||||||
await page.getByRole('button', { name: 'Generate Code' }).click();
|
await page.getByRole('button', { name: 'Generate Code' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('textbox', { name: 'Login Code' })).toHaveValue(
|
const link = await page.getByTestId('login-code-link').textContent();
|
||||||
/http:\/\/localhost\/lc\/.*/
|
await context.clearCookies();
|
||||||
);
|
|
||||||
|
await page.goto(link!);
|
||||||
|
await page.waitForURL('/settings/account');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Delete user', async ({ page }) => {
|
test('Delete user', async ({ page }) => {
|
||||||
await page.goto('/settings/admin/users');
|
await page.goto('/settings/admin/users');
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('row', { name: `${users.craig.firstname} ${users.craig.lastname}` })
|
.getByRole('row', {
|
||||||
|
name: `${users.craig.firstname} ${users.craig.lastname}`
|
||||||
|
})
|
||||||
.getByRole('button')
|
.getByRole('button')
|
||||||
.click();
|
.click();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
@@ -81,7 +87,9 @@ test('Delete user', async ({ page }) => {
|
|||||||
|
|
||||||
await expect(page.getByRole('status')).toHaveText('User deleted successfully');
|
await expect(page.getByRole('status')).toHaveText('User deleted successfully');
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('row', { name: `${users.craig.firstname} ${users.craig.lastname}` })
|
page.getByRole('row', {
|
||||||
|
name: `${users.craig.firstname} ${users.craig.lastname}`
|
||||||
|
})
|
||||||
).not.toBeVisible();
|
).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user