@podosoft/podokit 1.5.0 → 1.6.0
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/README.md +2 -1
- package/dist/app-proxy-timeouts.d.ts +27 -0
- package/dist/app-proxy-timeouts.js +180 -0
- package/dist/assemble.d.ts +5 -2
- package/dist/assemble.js +14 -4
- package/dist/deploy-checks.d.ts +44 -0
- package/dist/deploy-checks.js +267 -0
- package/dist/deploy-compose-profile.d.ts +6 -2
- package/dist/deploy-compose-profile.js +15 -6
- package/dist/deploy-compose-render.d.ts +12 -4
- package/dist/deploy-compose-render.js +47 -18
- package/dist/deploy-compose.d.ts +2 -5
- package/dist/deploy-compose.js +7 -2
- package/dist/deploy-profile.d.ts +6 -2
- package/dist/deploy-profile.js +11 -7
- package/dist/deploy-render.d.ts +4 -1
- package/dist/deploy-render.js +27 -21
- package/dist/deploy-schema.d.ts +56 -2
- package/dist/deploy-schema.js +130 -14
- package/dist/deploy.d.ts +2 -5
- package/dist/deploy.js +5 -2
- package/dist/dev.d.ts +21 -0
- package/dist/dev.js +78 -3
- package/dist/eject.js +2 -0
- package/dist/index.js +85 -5
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +5 -2
- package/dist/lockfile.d.ts +24 -0
- package/dist/lockfile.js +74 -4
- package/dist/provider.js +10 -5
- package/dist/remove.js +1 -1
- package/dist/template-files.d.ts +42 -0
- package/dist/template-files.js +102 -0
- package/dist/templates/fullstack/AGENTS.md +28 -4
- package/dist/templates/fullstack/README.md +8 -7
- package/dist/templates/fullstack/apps/api/package.json +4 -4
- package/dist/templates/fullstack/apps/api/scripts/build.extensions.mjs +6 -0
- package/dist/templates/fullstack/apps/api/scripts/build.mjs +65 -16
- package/dist/templates/fullstack/apps/api/scripts/run-migrations.mjs +7 -3
- package/dist/templates/fullstack/apps/api/scripts/verify-api-contract.ts +5 -1
- package/dist/templates/fullstack/apps/api/src/app.spec.ts +22 -0
- package/dist/templates/fullstack/apps/api/src/app.ts +41 -25
- package/dist/templates/fullstack/apps/api/src/build.spec.ts +118 -0
- package/dist/templates/fullstack/apps/api/src/config/env.extensions.ts +20 -0
- package/dist/templates/fullstack/apps/api/src/config/env.validation.spec.ts +76 -0
- package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +18 -3
- package/dist/templates/fullstack/apps/api/src/config/provider-selection.spec.ts +53 -0
- package/dist/templates/fullstack/apps/api/src/config/provider-selection.ts +79 -0
- package/dist/templates/fullstack/apps/api/src/config/providers.ts +7 -3
- package/dist/templates/fullstack/apps/api/src/core/api-contract.extensions.ts +6 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.spec.ts +29 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +39 -2
- package/dist/templates/fullstack/apps/api/src/core/services.ts +13 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +27 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.ts +7 -10
- package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +12 -0
- package/dist/templates/fullstack/apps/api/src/database/database.ts +2 -3
- package/dist/templates/fullstack/apps/api/src/database/entities.ts +12 -0
- package/dist/templates/fullstack/apps/api/src/database/migrate.ts +12 -2
- package/dist/templates/fullstack/apps/api/src/database/migration-files.spec.ts +116 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-files.ts +134 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-readiness.spec.ts +36 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-readiness.ts +25 -0
- package/dist/templates/fullstack/apps/api/src/database/run-migrations.spec.ts +12 -1
- package/dist/templates/fullstack/apps/api/src/database/sqlite-migrator.ts +2 -36
- package/dist/templates/fullstack/apps/api/src/main.ts +2 -2
- package/dist/templates/fullstack/apps/api/tsconfig.json +2 -0
- package/dist/templates/fullstack/apps/web/package.json +1 -1
- package/dist/templates/fullstack/apps/web/src/app.css +3 -0
- package/dist/templates/fullstack/apps/web/src/app.extensions.css +11 -0
- package/dist/templates/fullstack/apps/web/src/lib/components/site-runtime.svelte +3 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/api.test.ts +19 -0
- package/dist/templates/fullstack/apps/web/src/lib/server/api.ts +4 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +79 -0
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +55 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/proxy-timeouts.ts +2 -1
- package/dist/templates/fullstack/apps/web/tsconfig.json +1 -1
- package/dist/templates/fullstack/apps/web/vite.config.test.ts +33 -0
- package/dist/templates/fullstack/apps/web/vite.config.ts +55 -13
- package/dist/templates/fullstack/apps/web/vite.extensions.ts +23 -0
- package/dist/templates/fullstack/compose.dev.yaml +7 -4
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/SKILL.md +2 -2
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +12 -3
- package/dist/templates/fullstack/dot-claude/skills/podokit-update/SKILL.md +5 -2
- package/dist/templates/fullstack/dot-env.docker.example +5 -2
- package/dist/templates/fullstack/dot-env.example +4 -0
- package/dist/templates/fullstack/dot-podokit/dev.json +1 -1
- package/dist/templates/fullstack/infra/k3s/configmap.yaml +1 -0
- package/dist/templates/fullstack/tests/README.md +40 -1
- package/dist/templates/fullstack/tests/helpers/app-paths.ts +18 -0
- package/dist/templates/fullstack/tests/helpers/paths.ts +33 -0
- package/dist/templates/fullstack/tests/package.json +2 -2
- package/dist/templates/fullstack/tests/playwright.config.ts +7 -3
- package/dist/templates/fullstack/tests/playwright.extensions.ts +88 -1
- package/dist/templates/fullstack/tests/ui/smoke.ui.spec.ts +5 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +37 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +15 -6
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.test.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flags.ts +12 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-nav.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.test.ts +39 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.ts +35 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +15 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +11 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/admin-sidebar.svelte +2 -23
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +7 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.test.ts +30 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/impersonation-banner.svelte +3 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/settings/app-flags-settings.svelte +65 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +32 -23
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.test.ts +27 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-footer.svelte +4 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +3 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/app-hooks.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/non-indexable-paths.ts +8 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.test.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.ts +5 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts +2 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.svelte +1 -5
- package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +159 -170
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +72 -81
- package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +1 -6
- package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +43 -53
- package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +3 -1
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +24 -6
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/app-accounts.ts +24 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/devices.ts +41 -11
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -3
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +3 -9
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +2 -4
- package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +11 -5
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account-flows.ui.spec.ts +4 -5
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +55 -55
- package/dist/templates/modules/admin-dashboard/files/tests/ui/app-flags.ui.spec.ts +63 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +12 -16
- package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +3 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +32 -35
- package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +4 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +1 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +82 -91
- package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +6 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +16 -20
- package/dist/templates/modules/admin-dashboard/files/tests/ui/impersonation.ui.spec.ts +6 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +2 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +2 -4
- package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +5 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +7 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/same-origin-requests.ui.spec.ts +89 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +5 -4
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +67 -73
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +84 -93
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +8 -7
- package/dist/templates/modules/admin-dashboard/files/tests/ui/theme-mode.ui.spec.ts +29 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +51 -54
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +10 -9
- package/dist/templates/modules/admin-dashboard/module.manifest.json +7 -2
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +5 -1
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +17 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +18 -0
- package/dist/templates/modules/audit-log/module.manifest.json +15 -3
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-app-name.spec.ts +21 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +13 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-environment.ts +9 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +19 -3
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +2 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.runtime.spec.ts +10 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +7 -5
- package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.spec.ts +86 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.ts +44 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +2 -1
- package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +3 -1
- package/dist/templates/modules/auth/files/apps/api/src/settings/app-flags.ts +9 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.spec.ts +43 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +30 -5
- package/dist/templates/modules/auth/module.manifest.json +8 -2
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +57 -1
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +93 -9
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.spec.ts +40 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +52 -5
- package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +9 -7
- package/dist/templates/modules/bullmq/files/apps/api/src/worker.extensions.ts +13 -0
- package/dist/templates/modules/bullmq/module.manifest.json +5 -1
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +2 -0
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +87 -1
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +93 -3
- package/dist/templates/modules/jobs-local/module.manifest.json +3 -0
- package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.spec.ts +16 -0
- package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.ts +15 -10
- package/dist/templates/modules/mailer/module.manifest.json +1 -1
- package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +1 -1
- package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -4
- package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +1 -1
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +45 -20
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +1 -1
- package/dist/templates/todo/AGENTS.md +5 -1
- package/dist/templates/todo/README.md +6 -4
- package/dist/text-diff.d.ts +12 -0
- package/dist/text-diff.js +111 -0
- package/dist/update.d.ts +61 -3
- package/dist/update.js +152 -27
- package/package.json +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/api/exclusive.api.spec.ts +0 -75
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/exclusive.ts +0 -80
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { USER } from "../helpers/accounts";
|
|
2
3
|
import { ready } from "../helpers/hydration";
|
|
3
4
|
|
|
4
5
|
// admin storageState (project default) — organizations is an admin-only view
|
|
@@ -38,7 +39,9 @@ test("create an org with a non-ascii name, a parent, and a manager", async ({ pa
|
|
|
38
39
|
await page.getByLabel("Name", { exact: true }).fill(child);
|
|
39
40
|
await page.locator("#o-parent").click();
|
|
40
41
|
await page.getByRole("option", { name: parent }).click();
|
|
41
|
-
|
|
42
|
+
// The seeded user, not the first listed one: tests on other workers add and
|
|
43
|
+
// remove users while this one runs.
|
|
44
|
+
await page.locator("label", { hasText: USER.email }).getByRole("checkbox").click();
|
|
42
45
|
await page.getByRole("dialog").getByRole("button", { name: "Create", exact: true }).click();
|
|
43
46
|
await expect(page.getByText("Organization created").first()).toBeVisible();
|
|
44
47
|
// child row shows the parent org name
|
|
@@ -64,7 +67,7 @@ test("organization manage: remove a member, invite one, and cancel the invite",
|
|
|
64
67
|
await page.getByLabel("Name", { exact: true }).fill(name);
|
|
65
68
|
// Seed a specific non-owner member (the admin is always the owner, so pick the
|
|
66
69
|
// seeded user — .first() is order-dependent and could hit the owner).
|
|
67
|
-
await page.locator("label", { hasText:
|
|
70
|
+
await page.locator("label", { hasText: USER.email }).getByRole("checkbox").click();
|
|
68
71
|
await page.getByRole("dialog").getByRole("button", { name: "Create", exact: true }).click();
|
|
69
72
|
await expect(page.getByText("Organization created").first()).toBeVisible();
|
|
70
73
|
|
|
@@ -29,9 +29,14 @@ test("register a passkey and sign in with it", async ({ page, baseURL }) => {
|
|
|
29
29
|
await addBtn.click();
|
|
30
30
|
await expect(page.getByText("Passkey added")).toBeVisible();
|
|
31
31
|
|
|
32
|
-
// Sign out, then sign back in using only the passkey.
|
|
33
|
-
|
|
32
|
+
// Sign out, then sign back in using only the passkey. Check the sign-out: a
|
|
33
|
+
// session left behind would pass the URL check below without the passkey.
|
|
34
|
+
const signOut = await page.context().request.post("/api/auth/sign-out", { headers: origin, data: {} });
|
|
35
|
+
expect(signOut.ok(), await signOut.text()).toBeTruthy();
|
|
36
|
+
expect(await (await page.context().request.get("/api/auth/get-session")).json()).toBeNull();
|
|
34
37
|
await ready(page, "/login");
|
|
35
38
|
await page.getByRole("button", { name: "Use a passkey" }).click();
|
|
36
39
|
await expect(page).toHaveURL(new URL("/", baseURL).toString());
|
|
40
|
+
const session = await (await page.context().request.get("/api/auth/get-session")).json();
|
|
41
|
+
expect(session?.user?.email).toBe(email);
|
|
37
42
|
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { expect } from "@playwright/test";
|
|
2
|
+
import { test } from "../helpers/disposable-users";
|
|
3
|
+
import { ready } from "../helpers/hydration";
|
|
4
|
+
|
|
5
|
+
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
6
|
+
const origin = new URL(base).origin;
|
|
7
|
+
// A 1 by 1 pixel PNG.
|
|
8
|
+
const PIXEL_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
|
|
9
|
+
|
|
10
|
+
// SvelteKit answers 403 "Cross-site ... form submissions are forbidden" to a POST,
|
|
11
|
+
// PUT, PATCH, or DELETE that has no content type or one an HTML form can send,
|
|
12
|
+
// unless its Origin header names the origin the web server resolved. The browser
|
|
13
|
+
// sends Origin with every same-origin request below, so FormData uploads and
|
|
14
|
+
// requests without a body reach the API through the /api proxy as they are. When
|
|
15
|
+
// a deployment returns that 403 anyway, the web server resolved another origin,
|
|
16
|
+
// such as http:// behind a TLS proxy whose X-Forwarded-Proto the ingress replaced.
|
|
17
|
+
test("same-origin uploads and requests without a body pass the origin check", async ({
|
|
18
|
+
browser,
|
|
19
|
+
playwright,
|
|
20
|
+
disposableUsers,
|
|
21
|
+
}) => {
|
|
22
|
+
const email = `same-origin-${Date.now()}@example.com`;
|
|
23
|
+
const password = "Same-origin-password-1";
|
|
24
|
+
await disposableUsers.create({ email, password, name: "Same origin" });
|
|
25
|
+
// Start without the project's administrator cookies: with the multi-session
|
|
26
|
+
// plugin, signing out ends every session the browser holds.
|
|
27
|
+
const signIn = await playwright.request.newContext({
|
|
28
|
+
baseURL: base,
|
|
29
|
+
extraHTTPHeaders: { origin },
|
|
30
|
+
storageState: { cookies: [], origins: [] },
|
|
31
|
+
});
|
|
32
|
+
expect((await signIn.post("/api/auth/sign-in/email", { data: { email, password } })).ok()).toBeTruthy();
|
|
33
|
+
const context = await browser.newContext({ storageState: await signIn.storageState() });
|
|
34
|
+
await signIn.dispose();
|
|
35
|
+
const page = await context.newPage();
|
|
36
|
+
try {
|
|
37
|
+
await ready(page, "/account");
|
|
38
|
+
const origins: Record<string, string | undefined> = {};
|
|
39
|
+
const recorded: Promise<void>[] = [];
|
|
40
|
+
page.on("request", (request) => {
|
|
41
|
+
const { pathname } = new URL(request.url());
|
|
42
|
+
if (!pathname.startsWith("/api/")) return;
|
|
43
|
+
recorded.push(request.allHeaders().then((headers) => {
|
|
44
|
+
origins[`${request.method()} ${pathname}`] = headers.origin;
|
|
45
|
+
}));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const results = await page.evaluate(async (png) => {
|
|
49
|
+
const bytes = Uint8Array.from(atob(png), (char) => char.charCodeAt(0));
|
|
50
|
+
const form = new FormData();
|
|
51
|
+
form.set("file", new Blob([bytes], { type: "image/png" }), "pixel.png");
|
|
52
|
+
const answer = async (response: Response): Promise<{ status: number; body: string }> => ({
|
|
53
|
+
status: response.status,
|
|
54
|
+
body: await response.text(),
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
upload: await answer(await fetch("/api/account/profile-image", { method: "POST", body: form })),
|
|
58
|
+
remove: await answer(await fetch("/api/account/profile-image", { method: "DELETE" })),
|
|
59
|
+
urlencoded: await answer(
|
|
60
|
+
await fetch("/api/account/profile-image", {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
63
|
+
body: "file=pixel",
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
signOut: await answer(await fetch("/api/auth/sign-out", { method: "POST" })),
|
|
67
|
+
};
|
|
68
|
+
}, PIXEL_PNG);
|
|
69
|
+
|
|
70
|
+
// The multipart upload and the DELETE without a body reached the API and succeeded.
|
|
71
|
+
expect(results.upload.status, results.upload.body).toBe(201);
|
|
72
|
+
expect(JSON.parse(results.upload.body)).toMatchObject({ image: expect.stringContaining("/api/") });
|
|
73
|
+
expect(results.remove.status, results.remove.body).toBe(200);
|
|
74
|
+
// A form-encoded body is not an image, so the API, not SvelteKit, refuses it.
|
|
75
|
+
expect(results.urlencoded.body).not.toContain("Cross-site");
|
|
76
|
+
expect(JSON.parse(results.urlencoded.body)).toMatchObject({ success: false });
|
|
77
|
+
// A POST without a body or content type signs the user out.
|
|
78
|
+
expect(results.signOut.status, results.signOut.body).toBe(200);
|
|
79
|
+
|
|
80
|
+
await Promise.all(recorded);
|
|
81
|
+
expect(origins).toMatchObject({
|
|
82
|
+
"POST /api/account/profile-image": origin,
|
|
83
|
+
"DELETE /api/account/profile-image": origin,
|
|
84
|
+
"POST /api/auth/sign-out": origin,
|
|
85
|
+
});
|
|
86
|
+
} finally {
|
|
87
|
+
await context.close();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "../helpers/disposable-users";
|
|
2
|
+
import { USER } from "../helpers/accounts";
|
|
2
3
|
import { ready } from "../helpers/hydration";
|
|
3
4
|
|
|
4
5
|
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
@@ -36,14 +37,14 @@ test("admin sees user sessions across users", async ({ page }) => {
|
|
|
36
37
|
// Search by exact address — robust to pagination as sessions accumulate (the
|
|
37
38
|
// admin account alone can fill the first page). This is the across-users view.
|
|
38
39
|
const searched = page.waitForResponse(
|
|
39
|
-
(response) => isSessionList(response.url()) && new URL(response.url()).searchParams.get("search") ===
|
|
40
|
+
(response) => isSessionList(response.url()) && new URL(response.url()).searchParams.get("search") === USER.email,
|
|
40
41
|
);
|
|
41
|
-
await page.locator("#toolbar-search").fill(
|
|
42
|
+
await page.locator("#toolbar-search").fill(USER.email);
|
|
42
43
|
await page.getByRole("button", { name: "Search", exact: true }).click();
|
|
43
44
|
expect((await searched).ok()).toBeTruthy();
|
|
44
|
-
await expect(page.getByRole("main").getByText(
|
|
45
|
+
await expect(page.getByRole("main").getByText(USER.email).first()).toBeVisible();
|
|
45
46
|
// row actions are available
|
|
46
|
-
await page.getByRole("row", { name:
|
|
47
|
+
await page.getByRole("row", { name: USER.email }).first().getByRole("button").click();
|
|
47
48
|
await expect(page.getByRole("menuitem", { name: "Revoke session" })).toBeVisible();
|
|
48
49
|
await expect(page.getByRole("menuitem", { name: "Revoke all for user" })).toBeVisible();
|
|
49
50
|
});
|
|
@@ -1,60 +1,57 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
2
|
import { ready } from "../helpers/hydration";
|
|
3
|
-
import { exclusive } from "../helpers/exclusive";
|
|
4
3
|
|
|
5
4
|
// admin storageState (project default)
|
|
6
5
|
|
|
7
|
-
test("settings: add and remove a social login provider", async ({ page }) => {
|
|
6
|
+
test("settings: add and remove a social login provider", { tag: "@exclusive" }, async ({ page }) => {
|
|
8
7
|
test.slow();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
8
|
+
const dialog = page.getByRole("dialog");
|
|
9
|
+
const addBtn = dialog.getByRole("button", { name: "Add provider" });
|
|
10
|
+
const openConfigure = async (): Promise<void> => {
|
|
11
|
+
await page.waitForLoadState("networkidle");
|
|
12
|
+
await page.getByRole("tab", { name: "Authentication" }).click();
|
|
13
|
+
const socialCard = page
|
|
14
|
+
.getByText("Social login", { exact: true })
|
|
15
|
+
.locator('xpath=ancestor::div[@data-slot="card"]');
|
|
16
|
+
await socialCard.getByRole("button", { name: "Configure" }).click();
|
|
17
|
+
};
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
await ready(page, "/admin/settings");
|
|
20
|
+
// The auth-config catalog can be momentarily unavailable right after another
|
|
21
|
+
// spec toggles auth config (the auth instance rebuilds). Reload until the
|
|
22
|
+
// catalog is loaded (Add provider becomes enabled).
|
|
23
|
+
await expect(async () => {
|
|
24
|
+
await page.reload();
|
|
25
|
+
await openConfigure();
|
|
26
|
+
await expect(addBtn).toBeEnabled({ timeout: 3000 });
|
|
27
|
+
}).toPass({ timeout: 25000 });
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
29
|
+
await addBtn.click();
|
|
30
|
+
const provider = dialog.locator("#social-provider");
|
|
31
|
+
await expect(provider).toBeVisible();
|
|
32
|
+
const callback = await dialog.locator("code").textContent();
|
|
33
|
+
const providerId = callback?.trim().split("/").pop() ?? ""; // first addable provider (not hard-coded)
|
|
34
|
+
expect(providerId).not.toBe("");
|
|
35
|
+
const expectedCallback = new URL(`/api/auth/callback/${providerId}`, page.url()).toString();
|
|
36
|
+
expect(callback?.trim()).toBe(expectedCallback);
|
|
37
|
+
try {
|
|
38
|
+
await dialog.locator("#social-id").fill("test-client-id");
|
|
39
|
+
await dialog.locator("#social-secret").fill("test-secret");
|
|
40
|
+
await dialog.getByRole("button", { name: "Save", exact: true }).click();
|
|
41
|
+
// Save returns to the list view, where the new provider row has an enable switch.
|
|
42
|
+
await expect(dialog.getByRole("switch").first()).toBeVisible();
|
|
43
|
+
const saved = await (await page.request.get("/api/account/auth-config")).json();
|
|
44
|
+
expect(saved.social[providerId].redirectURI).toBe(expectedCallback);
|
|
45
|
+
} finally {
|
|
46
|
+
if (providerId) {
|
|
47
|
+
await page.request
|
|
48
|
+
.put("/api/account/auth-config", { data: { social: { [providerId]: { delete: true } } } })
|
|
49
|
+
.catch(() => undefined);
|
|
53
50
|
}
|
|
54
|
-
}
|
|
51
|
+
}
|
|
55
52
|
});
|
|
56
53
|
|
|
57
|
-
test("settings: open and save the SMTP dialog", async ({ page }) => {
|
|
54
|
+
test("settings: open and save the SMTP dialog", { tag: "@exclusive" }, async ({ page }) => {
|
|
58
55
|
await ready(page, "/admin/settings");
|
|
59
56
|
await page.getByRole("tab", { name: "Authentication" }).click();
|
|
60
57
|
await page.getByRole("button", { name: "SMTP settings" }).click();
|
|
@@ -74,35 +71,32 @@ test("settings: open and save the SMTP dialog", async ({ page }) => {
|
|
|
74
71
|
}
|
|
75
72
|
});
|
|
76
73
|
|
|
77
|
-
test("settings: configure automatic logout", async ({ page }) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
await ready(page, "/admin/settings");
|
|
84
|
-
await page.getByRole("tab", { name: "Authentication" }).click();
|
|
74
|
+
test("settings: configure automatic logout", { tag: "@exclusive" }, async ({ page }) => {
|
|
75
|
+
await page.request.put("/api/account/auth-config", {
|
|
76
|
+
data: { server: { sessionIdleTimeoutMinutes: null } },
|
|
77
|
+
});
|
|
78
|
+
await ready(page, "/admin/settings");
|
|
79
|
+
await page.getByRole("tab", { name: "Authentication" }).click();
|
|
85
80
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
const toggle = page.getByRole("switch", { name: "Automatic logout" });
|
|
82
|
+
await expect(toggle).not.toBeChecked();
|
|
83
|
+
await toggle.click();
|
|
84
|
+
await expect(toggle).toBeChecked();
|
|
90
85
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
86
|
+
const card = page
|
|
87
|
+
.getByText("Automatic logout", { exact: true })
|
|
88
|
+
.locator('xpath=ancestor::div[@data-slot="card"]');
|
|
89
|
+
await card.getByRole("button", { name: "Configure" }).click();
|
|
90
|
+
const dialog = page.getByRole("dialog");
|
|
91
|
+
await dialog.getByLabel("Idle time (minutes)").fill("45");
|
|
92
|
+
await dialog.getByRole("button", { name: "Save", exact: true }).click();
|
|
93
|
+
await expect(page.getByText("45 min", { exact: true })).toBeVisible();
|
|
99
94
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
95
|
+
const saved = await (await page.request.get("/api/account/auth-config")).json();
|
|
96
|
+
expect(saved.server.sessionIdleTimeoutMinutes).toBe(45);
|
|
97
|
+
const caps = await (await page.request.get("/api/account/capabilities")).json();
|
|
98
|
+
expect(caps.sessionIdleTimeoutMinutes).toBe(45);
|
|
104
99
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
});
|
|
100
|
+
await toggle.click();
|
|
101
|
+
await expect(toggle).not.toBeChecked();
|
|
108
102
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expect, test, type Page } from "@playwright/test";
|
|
2
2
|
import { ready } from "../helpers/hydration";
|
|
3
|
-
import {
|
|
3
|
+
import { paths } from "../helpers/paths";
|
|
4
4
|
|
|
5
5
|
// admin storageState (project default)
|
|
6
6
|
|
|
@@ -30,20 +30,17 @@ test("general settings: shows the favicon uploader", async ({ page }) => {
|
|
|
30
30
|
await expect(page.locator('input[type="file"]')).toBeVisible();
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
test("general settings: edit the site name, save, and apply live", async ({ page }) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
await name.fill(original);
|
|
45
|
-
await saveGeneral(page);
|
|
46
|
-
});
|
|
33
|
+
test("general settings: edit the site name, save, and apply live", { tag: "@exclusive" }, async ({ page }) => {
|
|
34
|
+
await ready(page, "/admin/settings");
|
|
35
|
+
const name = page.getByLabel("Site name");
|
|
36
|
+
const original = await name.inputValue();
|
|
37
|
+
await name.fill("PodoKit Test Site");
|
|
38
|
+
await saveGeneral(page);
|
|
39
|
+
// the browser tab title reflects the new name live (no reload)
|
|
40
|
+
await expect(page).toHaveTitle("PodoKit Test Site");
|
|
41
|
+
// restore so other specs start clean
|
|
42
|
+
await name.fill(original);
|
|
43
|
+
await saveGeneral(page);
|
|
47
44
|
});
|
|
48
45
|
|
|
49
46
|
test("settings lists the auth features under the Authentication tab", async ({ page }) => {
|
|
@@ -62,8 +59,8 @@ test("sidebar: back-to-home link returns to the landing page", async ({ page })
|
|
|
62
59
|
// the home link lives in the sidebar footer, above the user menu
|
|
63
60
|
await page.getByRole("link", { name: "Back to home" }).click();
|
|
64
61
|
await expect(page).toHaveURL(/\/$/);
|
|
65
|
-
//
|
|
66
|
-
await expect(
|
|
62
|
+
// The landing page is app-owned; tests/helpers/app-paths.ts says what shows it rendered.
|
|
63
|
+
await expect(paths.homeReady(page)).toBeVisible();
|
|
67
64
|
});
|
|
68
65
|
|
|
69
66
|
test("settings is reachable from the sidebar", async ({ page }) => {
|
|
@@ -72,22 +69,18 @@ test("settings is reachable from the sidebar", async ({ page }) => {
|
|
|
72
69
|
await expect(page).toHaveURL(/\/admin\/settings/);
|
|
73
70
|
});
|
|
74
71
|
|
|
75
|
-
test("admin can toggle a feature flag and it applies live @smoke", async ({ page }) => {
|
|
76
|
-
|
|
77
|
-
await
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// restore so the shared magic-link/2FA specs keep their expected state
|
|
88
|
-
await toggle.click();
|
|
89
|
-
await expect(toggle).toBeChecked();
|
|
90
|
-
});
|
|
72
|
+
test("admin can toggle a feature flag and it applies live @smoke", { tag: "@exclusive" }, async ({ page }) => {
|
|
73
|
+
await ready(page, "/admin/settings");
|
|
74
|
+
await page.getByRole("tab", { name: "Authentication" }).click();
|
|
75
|
+
const toggle = page.getByRole("switch", { name: "Magic link" });
|
|
76
|
+
test.skip((await toggle.count()) === 0, "magic link toggle not present");
|
|
77
|
+
await expect(toggle).toBeChecked();
|
|
78
|
+
await toggle.click();
|
|
79
|
+
await expect(page.getByText("Setting updated")).toBeVisible();
|
|
80
|
+
await expect(toggle).not.toBeChecked(); // capabilities refreshed live
|
|
81
|
+
// restore so the shared magic-link/2FA specs keep their expected state
|
|
82
|
+
await toggle.click();
|
|
83
|
+
await expect(toggle).toBeChecked();
|
|
91
84
|
});
|
|
92
85
|
|
|
93
86
|
test("settings: the require-two-factor toggle is available", async ({ page }) => {
|
|
@@ -101,72 +94,70 @@ test("settings: the require-two-factor toggle is available", async ({ page }) =>
|
|
|
101
94
|
await expect(page.getByRole("switch", { name: "Automatic logout" })).toBeVisible();
|
|
102
95
|
});
|
|
103
96
|
|
|
104
|
-
test("inactive browser sessions are signed out automatically @smoke", async ({ browser, page: adminPage }) => {
|
|
97
|
+
test("inactive browser sessions are signed out automatically @smoke", { tag: "@exclusive" }, async ({ browser, page: adminPage }) => {
|
|
105
98
|
test.slow();
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
99
|
+
// Idle sign-out revokes the active server session. Use a dedicated login so
|
|
100
|
+
// this test cannot invalidate the shared admin storageState used by later specs.
|
|
101
|
+
const context = await browser.newContext({ baseURL: base });
|
|
102
|
+
const page = await context.newPage();
|
|
103
|
+
const email = `idle-admin-${Date.now()}@example.com`;
|
|
104
|
+
let signOutRequests = 0;
|
|
105
|
+
let userId = "";
|
|
106
|
+
try {
|
|
107
|
+
const created = await adminPage.request.post("/api/auth/admin/create-user", {
|
|
108
|
+
headers: { origin: base },
|
|
109
|
+
data: { email, password: "Podokit3e-Str0ng!pw", name: "Idle Admin", role: "admin" },
|
|
110
|
+
});
|
|
111
|
+
expect(created.ok()).toBeTruthy();
|
|
112
|
+
userId = ((await created.json()).user?.id ?? "") as string;
|
|
113
|
+
expect(userId).toBeTruthy();
|
|
114
|
+
|
|
115
|
+
const signIn = await context.request.post("/api/auth/sign-in/email", {
|
|
116
|
+
headers: { origin: base },
|
|
117
|
+
data: { email, password: "Podokit3e-Str0ng!pw" },
|
|
118
|
+
});
|
|
119
|
+
expect(signIn.ok()).toBeTruthy();
|
|
120
|
+
|
|
121
|
+
const configured = await context.request.put("/api/account/auth-config", {
|
|
122
|
+
data: { server: { sessionIdleTimeoutMinutes: 5 } },
|
|
123
|
+
});
|
|
124
|
+
expect(configured.ok()).toBeTruthy();
|
|
125
|
+
await page.route("**/api/auth/sign-out", async (route) => {
|
|
126
|
+
signOutRequests += 1;
|
|
127
|
+
await route.fulfill({ status: 200, contentType: "application/json", body: '{"success":true}' });
|
|
128
|
+
});
|
|
129
|
+
await page.clock.install();
|
|
122
130
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
131
|
+
try {
|
|
132
|
+
await ready(page, "/admin");
|
|
133
|
+
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
|
|
134
|
+
} finally {
|
|
135
|
+
// Keep the loaded page policy at five minutes while restoring the shared
|
|
136
|
+
// DB policy before the isolated session is revoked.
|
|
137
|
+
const restored = await context.request.put("/api/account/auth-config", {
|
|
138
|
+
data: { server: { sessionIdleTimeoutMinutes: null } },
|
|
126
139
|
});
|
|
127
|
-
expect(
|
|
140
|
+
expect(restored.ok()).toBeTruthy();
|
|
141
|
+
}
|
|
128
142
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// Keep the loaded page policy at five minutes while restoring the shared
|
|
144
|
-
// DB policy before the isolated session is revoked.
|
|
145
|
-
const restored = await context.request.put("/api/account/auth-config", {
|
|
146
|
-
data: { server: { sessionIdleTimeoutMinutes: null } },
|
|
147
|
-
});
|
|
148
|
-
expect(restored.ok()).toBeTruthy();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const removed = await adminPage.request.post("/api/auth/admin/remove-user", {
|
|
143
|
+
const removed = await adminPage.request.post("/api/auth/admin/remove-user", {
|
|
144
|
+
headers: { origin: base },
|
|
145
|
+
data: { userId },
|
|
146
|
+
});
|
|
147
|
+
expect(removed.ok()).toBeTruthy();
|
|
148
|
+
userId = "";
|
|
149
|
+
|
|
150
|
+
await page.clock.fastForward(5 * 60_000 + 1_000);
|
|
151
|
+
await expect.poll(() => signOutRequests).toBe(1);
|
|
152
|
+
await expect(page).toHaveURL(/\/login\?reason=idle$/);
|
|
153
|
+
await expect(page.getByTestId("session-timeout-message")).toBeVisible();
|
|
154
|
+
} finally {
|
|
155
|
+
if (userId) {
|
|
156
|
+
await adminPage.request.post("/api/auth/admin/remove-user", {
|
|
152
157
|
headers: { origin: base },
|
|
153
158
|
data: { userId },
|
|
154
159
|
});
|
|
155
|
-
expect(removed.ok()).toBeTruthy();
|
|
156
|
-
userId = "";
|
|
157
|
-
|
|
158
|
-
await page.clock.fastForward(5 * 60_000 + 1_000);
|
|
159
|
-
await expect.poll(() => signOutRequests).toBe(1);
|
|
160
|
-
await expect(page).toHaveURL(/\/login\?reason=idle$/);
|
|
161
|
-
await expect(page.getByTestId("session-timeout-message")).toBeVisible();
|
|
162
|
-
} finally {
|
|
163
|
-
if (userId) {
|
|
164
|
-
await adminPage.request.post("/api/auth/admin/remove-user", {
|
|
165
|
-
headers: { origin: base },
|
|
166
|
-
data: { userId },
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
await context.close();
|
|
170
160
|
}
|
|
171
|
-
|
|
161
|
+
await context.close();
|
|
162
|
+
}
|
|
172
163
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { ADMIN, USER } from "../helpers/accounts";
|
|
2
3
|
import { ready } from "../helpers/hydration";
|
|
3
4
|
|
|
4
5
|
// The shared toolbar: filters + search both commit on the Search button; the
|
|
@@ -6,10 +7,10 @@ import { ready } from "../helpers/hydration";
|
|
|
6
7
|
test("users toolbar searches only when Search is pressed", async ({ page }) => {
|
|
7
8
|
await ready(page, "/admin/users");
|
|
8
9
|
const main = page.getByRole("main");
|
|
9
|
-
await page.locator("#toolbar-search").fill(
|
|
10
|
+
await page.locator("#toolbar-search").fill(ADMIN.email);
|
|
10
11
|
await page.getByRole("button", { name: "Search", exact: true }).click();
|
|
11
|
-
await expect(main.getByRole("cell", { name:
|
|
12
|
-
await expect(main.getByRole("cell", { name:
|
|
12
|
+
await expect(main.getByRole("cell", { name: ADMIN.email })).toBeVisible();
|
|
13
|
+
await expect(main.getByRole("cell", { name: USER.email })).toHaveCount(0);
|
|
13
14
|
});
|
|
14
15
|
|
|
15
16
|
test("users toolbar applies filter + search together on Search", async ({ page }) => {
|
|
@@ -24,8 +25,8 @@ test("users toolbar applies filter + search together on Search", async ({ page }
|
|
|
24
25
|
});
|
|
25
26
|
await page.getByRole("button", { name: "Search", exact: true }).click();
|
|
26
27
|
expect((await filtered).ok()).toBeTruthy();
|
|
27
|
-
await expect(main.getByRole("cell", { name:
|
|
28
|
-
await expect(main.getByRole("cell", { name:
|
|
28
|
+
await expect(main.getByRole("cell", { name: ADMIN.email })).toBeVisible();
|
|
29
|
+
await expect(main.getByRole("cell", { name: USER.email })).toHaveCount(0);
|
|
29
30
|
});
|
|
30
31
|
|
|
31
32
|
test("users toolbar combines the role and status filters", async ({ page }) => {
|
|
@@ -48,8 +49,8 @@ test("users toolbar combines the role and status filters", async ({ page }) => {
|
|
|
48
49
|
});
|
|
49
50
|
await page.getByRole("button", { name: "Search", exact: true }).click();
|
|
50
51
|
expect((await filtered).ok()).toBeTruthy();
|
|
51
|
-
await expect(main.getByRole("cell", { name:
|
|
52
|
-
await expect(main.getByRole("cell", { name:
|
|
52
|
+
await expect(main.getByRole("cell", { name: ADMIN.email })).toBeVisible();
|
|
53
|
+
await expect(main.getByRole("cell", { name: USER.email })).toHaveCount(0);
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
test("users toolbar can pick the search field", async ({ page }) => {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { ready } from "../helpers/hydration";
|
|
3
|
+
|
|
4
|
+
// mode-watcher marks <html> with `.dark` or `.light`, so component libraries
|
|
5
|
+
// that follow either class, and fall back to the OS setting otherwise, match the
|
|
6
|
+
// theme the visitor chose rather than the system one.
|
|
7
|
+
test("the chosen theme is named on <html> in both modes", async ({ page }) => {
|
|
8
|
+
await page.emulateMedia({ colorScheme: "dark" });
|
|
9
|
+
await ready(page, "/account");
|
|
10
|
+
const html = page.locator("html");
|
|
11
|
+
|
|
12
|
+
await page.getByRole("button", { name: "Toggle theme" }).click();
|
|
13
|
+
await page.getByRole("menuitem", { name: "Light" }).click();
|
|
14
|
+
await expect(html).toHaveClass(/(^|\s)light(\s|$)/);
|
|
15
|
+
await expect(html).not.toHaveClass(/(^|\s)dark(\s|$)/);
|
|
16
|
+
|
|
17
|
+
// The class is set before the page renders, not only after a choice.
|
|
18
|
+
await page.reload();
|
|
19
|
+
await expect(html).toHaveClass(/(^|\s)light(\s|$)/);
|
|
20
|
+
|
|
21
|
+
await page.getByRole("button", { name: "Toggle theme" }).click();
|
|
22
|
+
await page.getByRole("menuitem", { name: "Dark" }).click();
|
|
23
|
+
await expect(html).toHaveClass(/(^|\s)dark(\s|$)/);
|
|
24
|
+
await expect(html).not.toHaveClass(/(^|\s)light(\s|$)/);
|
|
25
|
+
|
|
26
|
+
await page.getByRole("button", { name: "Toggle theme" }).click();
|
|
27
|
+
await page.getByRole("menuitem", { name: "System" }).click();
|
|
28
|
+
await expect(html).toHaveClass(/(^|\s)dark(\s|$)/);
|
|
29
|
+
});
|