@podosoft/podokit 1.5.0 → 1.6.1
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 +60 -2
- package/dist/deploy-schema.js +141 -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 +30 -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 +7 -2
- 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 +121 -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 +72 -4
- package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +78 -6
- 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 +13 -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 +50 -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/helpers/seeded-admin.ts +15 -0
- package/dist/templates/fullstack/tests/package.json +3 -3
- package/dist/templates/fullstack/tests/playwright.config.ts +10 -4
- package/dist/templates/fullstack/tests/playwright.extensions.ts +149 -1
- package/dist/templates/fullstack/tests/run-e2e.ts +28 -0
- 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 +53 -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/components/theme-toggle.svelte +1 -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/hydration.ts +13 -4
- 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 +14 -6
- 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 +35 -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 +107 -1
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +113 -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 +174 -1
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +150 -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 +3 -6
- 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 +3 -6
- 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 +3 -6
- 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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { expect, test, type Page } from "@playwright/test";
|
|
2
|
+
import { APP_FLAG_SETTINGS } from "../../apps/web/src/lib/admin/app-flags";
|
|
3
|
+
import { ready } from "../helpers/hydration";
|
|
4
|
+
|
|
5
|
+
// admin storageState (project default). The Application tab lists the flags of
|
|
6
|
+
// the app-owned apps/web/src/lib/admin/app-flags.ts; a generated app has none.
|
|
7
|
+
// "only admins can change settings" covers the endpoint's refusal for other users.
|
|
8
|
+
const listed = APP_FLAG_SETTINGS.map((setting) => setting.flag);
|
|
9
|
+
|
|
10
|
+
async function appFlags(page: Page): Promise<Record<string, boolean>> {
|
|
11
|
+
const response = await page.request.get("/api/account/capabilities");
|
|
12
|
+
expect(response.ok()).toBeTruthy();
|
|
13
|
+
return ((await response.json()) as { appFlags?: Record<string, boolean> }).appFlags ?? {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test("the Application tab shows the flags app-flags.ts lists, and only then", async ({ page }) => {
|
|
17
|
+
await ready(page, "/admin/settings");
|
|
18
|
+
await expect(page.getByRole("tab", { name: "Authentication" })).toBeVisible();
|
|
19
|
+
const tab = page.getByRole("tab", { name: "Application" });
|
|
20
|
+
if (listed.length === 0) {
|
|
21
|
+
await expect(tab).toHaveCount(0);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
await tab.click();
|
|
25
|
+
const cards = page.locator("[data-app-flag]");
|
|
26
|
+
await expect(cards).toHaveCount(listed.length);
|
|
27
|
+
expect(await cards.evaluateAll((elements) => elements.map((element) => element.getAttribute("data-app-flag"))))
|
|
28
|
+
.toEqual(listed);
|
|
29
|
+
const flags = await appFlags(page);
|
|
30
|
+
for (const flag of listed) {
|
|
31
|
+
expect(flags, `${flag} is in APP_FLAG_DEFAULTS`).toHaveProperty(flag);
|
|
32
|
+
await expect(page.locator(`[data-app-flag="${flag}"]`).getByRole("switch"))
|
|
33
|
+
.toHaveAttribute("aria-checked", String(flags[flag]));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("an application flag switch saves through the settings endpoint", { tag: "@exclusive" }, async ({ page }) => {
|
|
38
|
+
test.skip(listed.length === 0, "app-flags.ts lists no application flags");
|
|
39
|
+
const flag = listed[0] ?? "";
|
|
40
|
+
const original = (await appFlags(page))[flag] === true;
|
|
41
|
+
await ready(page, "/admin/settings");
|
|
42
|
+
await page.getByRole("tab", { name: "Application" }).click();
|
|
43
|
+
const toggle = page.locator(`[data-app-flag="${flag}"]`).getByRole("switch");
|
|
44
|
+
await expect(toggle).toHaveAttribute("aria-checked", String(original));
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const saved = page.waitForResponse(
|
|
48
|
+
(response) => response.url().endsWith("/api/account/settings") && response.request().method() === "PUT",
|
|
49
|
+
);
|
|
50
|
+
await toggle.click();
|
|
51
|
+
expect((await saved).ok()).toBeTruthy();
|
|
52
|
+
await expect(page.getByText("Setting updated").last()).toBeVisible();
|
|
53
|
+
await expect.poll(async () => (await appFlags(page))[flag]).toBe(!original);
|
|
54
|
+
|
|
55
|
+
// The stored value survives a reload.
|
|
56
|
+
await ready(page, "/admin/settings");
|
|
57
|
+
await page.getByRole("tab", { name: "Application" }).click();
|
|
58
|
+
await expect(page.locator(`[data-app-flag="${flag}"]`).getByRole("switch"))
|
|
59
|
+
.toHaveAttribute("aria-checked", String(!original));
|
|
60
|
+
} finally {
|
|
61
|
+
await page.request.put("/api/account/settings", { data: { [flag]: original } });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
@@ -1,6 +1,5 @@
|
|
|
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). The Appearance controls live in their
|
|
6
5
|
// own "Appearance" settings tab.
|
|
@@ -49,20 +48,17 @@ test("appearance: advanced reveals per-token pickers", async ({ page }) => {
|
|
|
49
48
|
await expect(page.getByLabel("Primary dark")).toBeVisible();
|
|
50
49
|
});
|
|
51
50
|
|
|
52
|
-
test("appearance: pick a preset, save, apply live, then restore @smoke", async ({ page }) => {
|
|
53
|
-
|
|
54
|
-
await
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// Applied globally via the scoped override stylesheet.
|
|
61
|
-
await expect.poll(async () => page.locator("#podokit-theme").count()).toBeGreaterThan(0);
|
|
51
|
+
test("appearance: pick a preset, save, apply live, then restore @smoke", { tag: "@exclusive" }, async ({ page }) => {
|
|
52
|
+
await openAppearance(page);
|
|
53
|
+
await page.getByRole("button", { name: "Slate" }).click();
|
|
54
|
+
await page.getByRole("button", { name: "Medium" }).click();
|
|
55
|
+
await page.getByRole("button", { name: "Save changes" }).click();
|
|
56
|
+
await expect(page.getByText("General settings saved.").first()).toBeVisible();
|
|
57
|
+
// Applied globally via the scoped override stylesheet.
|
|
58
|
+
await expect.poll(async () => page.locator("#podokit-theme").count()).toBeGreaterThan(0);
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
});
|
|
60
|
+
// "Restore defaults" reverts everything in one click and persists.
|
|
61
|
+
await page.getByRole("button", { name: "Restore defaults" }).click();
|
|
62
|
+
await expect(page.getByText("General settings saved.").first()).toBeVisible();
|
|
63
|
+
await expect.poll(async () => page.locator("#podokit-theme").count()).toBe(0);
|
|
68
64
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { ADMIN } from "../helpers/accounts";
|
|
2
3
|
import { ready } from "../helpers/hydration";
|
|
3
4
|
|
|
4
5
|
// admin storageState (project default). The Audit log page is gated by the
|
|
@@ -23,7 +24,7 @@ test("admin filters the audit log on the server", async ({ page }) => {
|
|
|
23
24
|
|
|
24
25
|
await page.locator('button[data-slot="select-trigger"]').first().click(); // Action filter
|
|
25
26
|
await page.getByRole("option", { name: "auth.login", exact: true }).click();
|
|
26
|
-
await page.locator("#toolbar-search").fill(
|
|
27
|
+
await page.locator("#toolbar-search").fill(ADMIN.email);
|
|
27
28
|
const filtered = page.waitForResponse((response) =>
|
|
28
29
|
response.url().includes("/api/audit-logs?") &&
|
|
29
30
|
response.url().includes("action=auth.login") &&
|
|
@@ -35,7 +36,7 @@ test("admin filters the audit log on the server", async ({ page }) => {
|
|
|
35
36
|
const actions = table.locator("tbody tr td:nth-child(3)");
|
|
36
37
|
await expect(actions.first()).toHaveText("auth.login");
|
|
37
38
|
for (const text of await actions.allInnerTexts()) expect(text).toBe("auth.login");
|
|
38
|
-
await expect(table.getByText(
|
|
39
|
+
await expect(table.getByText(ADMIN.email).first()).toBeVisible();
|
|
39
40
|
});
|
|
40
41
|
|
|
41
42
|
test("admin pages through the audit log", async ({ page }) => {
|
|
@@ -2,7 +2,6 @@ import { expect, test } from "@playwright/test";
|
|
|
2
2
|
import { safeAuthRedirect, withAuthRedirect } from "../../apps/web/src/lib/auth-redirect";
|
|
3
3
|
import { anonState, ADMIN } from "../helpers/accounts";
|
|
4
4
|
import { ready } from "../helpers/hydration";
|
|
5
|
-
import { exclusive } from "../helpers/exclusive";
|
|
6
5
|
|
|
7
6
|
test.use({ storageState: anonState });
|
|
8
7
|
|
|
@@ -66,43 +65,41 @@ test("closed social registration errors explain that existing users may sign in"
|
|
|
66
65
|
);
|
|
67
66
|
});
|
|
68
67
|
|
|
69
|
-
test("configured social providers appear on the login page", async ({ page, playwright }) => {
|
|
68
|
+
test("configured social providers appear on the login page", { tag: "@exclusive" }, async ({ page, playwright }) => {
|
|
70
69
|
test.slow();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
clientSecret: "dummy-google-client-secret",
|
|
88
|
-
},
|
|
70
|
+
const admin = await playwright.request.newContext({
|
|
71
|
+
baseURL: base,
|
|
72
|
+
extraHTTPHeaders: { origin: base },
|
|
73
|
+
});
|
|
74
|
+
await admin.post("/api/auth/sign-in/email", {
|
|
75
|
+
data: { email: ADMIN.email, password: ADMIN.password },
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
expect(
|
|
79
|
+
(await admin.put("/api/account/auth-config", {
|
|
80
|
+
data: {
|
|
81
|
+
social: {
|
|
82
|
+
google: {
|
|
83
|
+
enabled: true,
|
|
84
|
+
clientId: "dummy-google-client-id",
|
|
85
|
+
clientSecret: "dummy-google-client-secret",
|
|
89
86
|
},
|
|
90
87
|
},
|
|
91
|
-
}
|
|
92
|
-
).
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
88
|
+
},
|
|
89
|
+
})).ok(),
|
|
90
|
+
).toBeTruthy();
|
|
91
|
+
await expect
|
|
92
|
+
.poll(async () => {
|
|
93
|
+
await page.goto("/login");
|
|
94
|
+
return page.getByRole("button", { name: "Continue with Google" }).count();
|
|
95
|
+
}, { timeout: 8_000 })
|
|
96
|
+
.toBe(1);
|
|
97
|
+
} finally {
|
|
98
|
+
await admin.put("/api/account/auth-config", {
|
|
99
|
+
data: { social: { google: { enabled: false } } },
|
|
100
|
+
});
|
|
101
|
+
await admin.dispose();
|
|
102
|
+
}
|
|
106
103
|
});
|
|
107
104
|
|
|
108
105
|
test("forgot-password shows a sent confirmation", async ({ page }) => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
2
|
import { ADMIN } from "../helpers/accounts";
|
|
3
3
|
import { ready } from "../helpers/hydration";
|
|
4
|
+
import { paths } from "../helpers/paths";
|
|
4
5
|
|
|
5
6
|
// admin storageState (project default)
|
|
6
7
|
test("overview shows the signed-in admin @smoke", async ({ page }) => {
|
|
@@ -11,7 +12,8 @@ test("overview shows the signed-in admin @smoke", async ({ page }) => {
|
|
|
11
12
|
});
|
|
12
13
|
|
|
13
14
|
test("landing shows language, theme, and signed-in account actions", async ({ page }) => {
|
|
14
|
-
|
|
15
|
+
test.skip(paths.accountMenu === null, "no page renders the account menu (tests/helpers/app-paths.ts)");
|
|
16
|
+
await ready(page, paths.accountMenu ?? "/");
|
|
15
17
|
const language = page.getByRole("button", { name: /^(Language|언어)$/ });
|
|
16
18
|
const theme = page.getByRole("button", { name: /^(Toggle theme|테마 전환)$/ });
|
|
17
19
|
await expect(language).toBeVisible();
|
|
@@ -33,7 +35,7 @@ test("sidebar navigates to sessions and account @smoke", async ({ page }) => {
|
|
|
33
35
|
await expect(page.getByRole("heading", { name: "User sessions" })).toBeVisible();
|
|
34
36
|
// Account is reached from the user menu at the bottom of the sidebar, not the nav
|
|
35
37
|
await expect(page.getByRole("link", { name: "Account" })).toHaveCount(0);
|
|
36
|
-
await page.
|
|
38
|
+
await page.getByTestId("sidebar-user-menu").click();
|
|
37
39
|
await page.getByRole("menuitem", { name: "Account" }).click();
|
|
38
40
|
await expect(page).toHaveURL((url) => url.pathname === "/account");
|
|
39
41
|
await expect(page.getByRole("heading", { name: "Account" })).toBeVisible();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expect, test } from "../helpers/disposable-users";
|
|
2
2
|
import { ready } from "../helpers/hydration";
|
|
3
|
-
import {
|
|
3
|
+
import { mailpitReachable, waitForLink } from "../helpers/mailpit";
|
|
4
4
|
|
|
5
5
|
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
6
6
|
|
|
@@ -21,7 +21,6 @@ test("admin sends a password reset email to a user", async ({ page, disposableUs
|
|
|
21
21
|
await ready(page, "/admin/users");
|
|
22
22
|
const email = `admin-reset-${Date.now()}@example.com`;
|
|
23
23
|
await disposableUsers.create({ email, name: "AR" });
|
|
24
|
-
await clearMailpit();
|
|
25
24
|
await page.locator("#toolbar-search").fill(email);
|
|
26
25
|
await page.getByRole("button", { name: "Search", exact: true }).click();
|
|
27
26
|
await page.getByRole("row", { name: new RegExp(email) }).getByRole("button").click();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
2
|
import { anonState } from "../helpers/accounts";
|
|
3
3
|
import { ready } from "../helpers/hydration";
|
|
4
|
-
import {
|
|
4
|
+
import { mailpitReachable, waitForLink } from "../helpers/mailpit";
|
|
5
5
|
|
|
6
6
|
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
7
7
|
const origin = { origin: base };
|
|
@@ -20,7 +20,6 @@ test("forgot password: request, open the emailed link, set a new password, sign
|
|
|
20
20
|
// The happy path ends by signing in; when verification is required that would be
|
|
21
21
|
// blocked, so this UI walkthrough only applies with verification off.
|
|
22
22
|
test.skip(verificationOn, "email verification enabled");
|
|
23
|
-
await clearMailpit();
|
|
24
23
|
|
|
25
24
|
await ready(page, "/forgot-password");
|
|
26
25
|
await page.getByLabel("Email").fill(email);
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { expect, test, type Page } from "@playwright/test";
|
|
2
2
|
import { anonState } from "../helpers/accounts";
|
|
3
3
|
import { ready } from "../helpers/hydration";
|
|
4
|
-
import {
|
|
4
|
+
import { paths } from "../helpers/paths";
|
|
5
5
|
|
|
6
6
|
// Verifies that the General settings actually take effect (not just persist).
|
|
7
7
|
// The admin storageState (project default) drives the toggles; a fresh anonymous
|
|
8
|
-
// context checks the public-facing result.
|
|
9
|
-
//
|
|
10
|
-
// 1; restores run in a `finally` so a mid-test failure still leaves the shared
|
|
11
|
-
// site settings clean.
|
|
8
|
+
// context checks the public-facing result. Restores run in a `finally` so a
|
|
9
|
+
// mid-test failure still leaves the shared site settings clean.
|
|
12
10
|
|
|
13
11
|
async function saveGeneral(
|
|
14
12
|
page: Page,
|
|
@@ -30,110 +28,103 @@ async function saveGeneral(
|
|
|
30
28
|
await expect(page.getByText("General settings saved.").last()).toBeVisible();
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
test("general: empty locale displays the application default", async ({ page }) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const settingsUrl = `${origin}/api/site/settings`;
|
|
39
|
-
const previous = await (await page.request.get(settingsUrl)).json() as { locale?: string };
|
|
31
|
+
test("general: empty locale displays the application default", { tag: "@exclusive" }, async ({ page }) => {
|
|
32
|
+
await ready(page, "/admin/settings");
|
|
33
|
+
const origin = new URL(page.url()).origin;
|
|
34
|
+
const settingsUrl = `${origin}/api/site/settings`;
|
|
35
|
+
const previous = await (await page.request.get(settingsUrl)).json() as { locale?: string };
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
37
|
+
try {
|
|
38
|
+
expect((await page.request.put(settingsUrl, { data: { locale: "" } })).ok()).toBeTruthy();
|
|
39
|
+
await ready(page, "/admin/settings");
|
|
40
|
+
await expect(page.locator("#locale")).toContainText("English");
|
|
41
|
+
} finally {
|
|
42
|
+
await page.request.put(settingsUrl, { data: { locale: previous.locale ?? "" } });
|
|
43
|
+
}
|
|
49
44
|
});
|
|
50
45
|
|
|
51
|
-
test("general: footer text and links render on public pages", async ({ page, browser }) => {
|
|
46
|
+
test("general: footer text and links render on public pages", { tag: "@exclusive" }, async ({ page, browser }) => {
|
|
47
|
+
test.skip(paths.footer === null, "no public page renders the site footer (tests/helpers/app-paths.ts)");
|
|
52
48
|
test.slow();
|
|
53
|
-
await
|
|
49
|
+
await ready(page, "/admin/settings");
|
|
50
|
+
const origin = new URL(page.url()).origin;
|
|
51
|
+
const footer = page.getByLabel("Footer text");
|
|
52
|
+
const support = page.getByLabel("Support email");
|
|
53
|
+
const origFooter = await footer.inputValue();
|
|
54
|
+
const origSupport = await support.inputValue();
|
|
55
|
+
await saveGeneral(
|
|
56
|
+
page,
|
|
57
|
+
async () => {
|
|
58
|
+
await footer.fill("© 2026 PodoKit Test");
|
|
59
|
+
await support.fill("help@example.com");
|
|
60
|
+
},
|
|
61
|
+
{ footerText: "© 2026 PodoKit Test", supportEmail: "help@example.com" },
|
|
62
|
+
);
|
|
63
|
+
try {
|
|
64
|
+
const ctx = await browser.newContext({ storageState: anonState });
|
|
65
|
+
const anon = await ctx.newPage();
|
|
66
|
+
await anon.goto(`${origin}${paths.footer}`);
|
|
67
|
+
const footerBlock = anon.getByTestId("site-footer");
|
|
68
|
+
await expect(footerBlock.getByText("© 2026 PodoKit Test")).toBeVisible();
|
|
69
|
+
await expect(footerBlock.getByRole("link", { name: "Support" })).toHaveAttribute("href", "mailto:help@example.com");
|
|
70
|
+
await ctx.close();
|
|
71
|
+
} finally {
|
|
54
72
|
await ready(page, "/admin/settings");
|
|
55
|
-
const origin = new URL(page.url()).origin;
|
|
56
|
-
const footer = page.getByLabel("Footer text");
|
|
57
|
-
const support = page.getByLabel("Support email");
|
|
58
|
-
const origFooter = await footer.inputValue();
|
|
59
|
-
const origSupport = await support.inputValue();
|
|
60
73
|
await saveGeneral(
|
|
61
74
|
page,
|
|
62
75
|
async () => {
|
|
63
|
-
await
|
|
64
|
-
await
|
|
76
|
+
await page.getByLabel("Footer text").fill(origFooter);
|
|
77
|
+
await page.getByLabel("Support email").fill(origSupport);
|
|
65
78
|
},
|
|
66
|
-
{ footerText:
|
|
79
|
+
{ footerText: origFooter, supportEmail: origSupport },
|
|
67
80
|
);
|
|
68
|
-
|
|
69
|
-
const ctx = await browser.newContext({ storageState: anonState });
|
|
70
|
-
const anon = await ctx.newPage();
|
|
71
|
-
await anon.goto(`${origin}/login`);
|
|
72
|
-
await expect(anon.getByText("© 2026 PodoKit Test")).toBeVisible();
|
|
73
|
-
await expect(anon.getByRole("link", { name: "Support" })).toHaveAttribute("href", "mailto:help@example.com");
|
|
74
|
-
await ctx.close();
|
|
75
|
-
} finally {
|
|
76
|
-
await ready(page, "/admin/settings");
|
|
77
|
-
await saveGeneral(
|
|
78
|
-
page,
|
|
79
|
-
async () => {
|
|
80
|
-
await page.getByLabel("Footer text").fill(origFooter);
|
|
81
|
-
await page.getByLabel("Support email").fill(origSupport);
|
|
82
|
-
},
|
|
83
|
-
{ footerText: origFooter, supportEmail: origSupport },
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
81
|
+
}
|
|
87
82
|
});
|
|
88
83
|
|
|
89
|
-
test("general: maintenance mode holds non-admins on the maintenance page", async ({ page, browser }) => {
|
|
84
|
+
test("general: maintenance mode holds non-admins on the maintenance page", { tag: "@exclusive" }, async ({ page, browser }) => {
|
|
90
85
|
test.slow();
|
|
91
|
-
await
|
|
86
|
+
await ready(page, "/admin/settings");
|
|
87
|
+
const origin = new URL(page.url()).origin;
|
|
88
|
+
await page.getByRole("switch", { name: "Maintenance mode" }).click();
|
|
89
|
+
await saveGeneral(page);
|
|
90
|
+
try {
|
|
91
|
+
const ctx = await browser.newContext({ storageState: anonState });
|
|
92
|
+
const anon = await ctx.newPage();
|
|
93
|
+
await anon.goto(`${origin}${paths.home}`);
|
|
94
|
+
await expect(anon).toHaveURL(/\/maintenance$/);
|
|
95
|
+
await expect(anon.getByRole("heading", { name: "Under maintenance" })).toBeVisible();
|
|
96
|
+
await ctx.close();
|
|
97
|
+
// the admin is not held back
|
|
98
|
+
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();
|
|
99
|
+
} finally {
|
|
92
100
|
await ready(page, "/admin/settings");
|
|
93
|
-
const
|
|
94
|
-
await
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const ctx = await browser.newContext({ storageState: anonState });
|
|
98
|
-
const anon = await ctx.newPage();
|
|
99
|
-
await anon.goto(`${origin}/`);
|
|
100
|
-
await expect(anon).toHaveURL(/\/maintenance$/);
|
|
101
|
-
await expect(anon.getByRole("heading", { name: "Under maintenance" })).toBeVisible();
|
|
102
|
-
await ctx.close();
|
|
103
|
-
// the admin is not held back
|
|
104
|
-
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();
|
|
105
|
-
} finally {
|
|
106
|
-
await ready(page, "/admin/settings");
|
|
107
|
-
const t = page.getByRole("switch", { name: "Maintenance mode" });
|
|
108
|
-
if (await t.isChecked()) {
|
|
109
|
-
await t.click();
|
|
110
|
-
await saveGeneral(page);
|
|
111
|
-
}
|
|
101
|
+
const t = page.getByRole("switch", { name: "Maintenance mode" });
|
|
102
|
+
if (await t.isChecked()) {
|
|
103
|
+
await t.click();
|
|
104
|
+
await saveGeneral(page);
|
|
112
105
|
}
|
|
113
|
-
}
|
|
106
|
+
}
|
|
114
107
|
});
|
|
115
108
|
|
|
116
|
-
test("general: closing sign-up redirects /signup and hides the link", async ({ page, browser }) => {
|
|
109
|
+
test("general: closing sign-up redirects /signup and hides the link", { tag: "@exclusive" }, async ({ page, browser }) => {
|
|
117
110
|
test.slow();
|
|
118
|
-
await
|
|
111
|
+
await ready(page, "/admin/settings");
|
|
112
|
+
const origin = new URL(page.url()).origin;
|
|
113
|
+
await page.getByRole("switch", { name: "Allow public sign-up" }).click();
|
|
114
|
+
await saveGeneral(page);
|
|
115
|
+
try {
|
|
116
|
+
const ctx = await browser.newContext({ storageState: anonState });
|
|
117
|
+
const anon = await ctx.newPage();
|
|
118
|
+
await anon.goto(`${origin}/signup`);
|
|
119
|
+
await expect(anon).toHaveURL(/\/login/);
|
|
120
|
+
await expect(anon.getByRole("link", { name: "Sign up" })).toHaveCount(0);
|
|
121
|
+
await ctx.close();
|
|
122
|
+
} finally {
|
|
119
123
|
await ready(page, "/admin/settings");
|
|
120
|
-
const
|
|
121
|
-
await
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const ctx = await browser.newContext({ storageState: anonState });
|
|
125
|
-
const anon = await ctx.newPage();
|
|
126
|
-
await anon.goto(`${origin}/signup`);
|
|
127
|
-
await expect(anon).toHaveURL(/\/login/);
|
|
128
|
-
await expect(anon.getByRole("link", { name: "Sign up" })).toHaveCount(0);
|
|
129
|
-
await ctx.close();
|
|
130
|
-
} finally {
|
|
131
|
-
await ready(page, "/admin/settings");
|
|
132
|
-
const t = page.getByRole("switch", { name: "Allow public sign-up" });
|
|
133
|
-
if (!(await t.isChecked())) {
|
|
134
|
-
await t.click();
|
|
135
|
-
await saveGeneral(page);
|
|
136
|
-
}
|
|
124
|
+
const t = page.getByRole("switch", { name: "Allow public sign-up" });
|
|
125
|
+
if (!(await t.isChecked())) {
|
|
126
|
+
await t.click();
|
|
127
|
+
await saveGeneral(page);
|
|
137
128
|
}
|
|
138
|
-
}
|
|
129
|
+
}
|
|
139
130
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
2
|
import { anonState, userState } from "../helpers/accounts";
|
|
3
|
+
import { paths } from "../helpers/paths";
|
|
3
4
|
|
|
4
5
|
test.describe("unauthenticated", () => {
|
|
5
6
|
test.use({ storageState: anonState });
|
|
@@ -16,8 +17,11 @@ test.describe("unauthenticated", () => {
|
|
|
16
17
|
expect(response.headers()["x-robots-tag"]).toBe("noindex, nofollow");
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
// Public pages come from tests/helpers/app-paths.ts.
|
|
21
|
+
for (const route of paths.indexable) {
|
|
22
|
+
const response = await request.get(route);
|
|
23
|
+
expect(response.headers()["x-robots-tag"], route).toBeUndefined();
|
|
24
|
+
}
|
|
21
25
|
});
|
|
22
26
|
});
|
|
23
27
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
2
|
import { ADMIN, adminState, anonState } from "../helpers/accounts";
|
|
3
3
|
import { ready } from "../helpers/hydration";
|
|
4
|
-
import { exclusive } from "../helpers/exclusive";
|
|
5
4
|
|
|
6
5
|
test.use({ storageState: anonState });
|
|
7
6
|
|
|
@@ -23,27 +22,24 @@ test("language switch updates messages and <html lang>", async ({ page }) => {
|
|
|
23
22
|
}
|
|
24
23
|
});
|
|
25
24
|
|
|
26
|
-
test("explicit English overrides a Korean site fallback", async ({ page, playwright }) => {
|
|
27
|
-
|
|
28
|
-
await
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
});
|
|
33
|
-
const previous = await (await admin.get("/api/site/settings")).json() as { locale?: string };
|
|
25
|
+
test("explicit English overrides a Korean site fallback", { tag: "@exclusive" }, async ({ page, playwright }) => {
|
|
26
|
+
const admin = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
27
|
+
await admin.post("/api/auth/sign-in/email", {
|
|
28
|
+
data: { email: ADMIN.email, password: ADMIN.password },
|
|
29
|
+
});
|
|
30
|
+
const previous = await (await admin.get("/api/site/settings")).json() as { locale?: string };
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
try {
|
|
33
|
+
expect((await admin.put("/api/site/settings", { data: { locale: "ko" } })).ok()).toBeTruthy();
|
|
34
|
+
await page.context().addCookies([{ name: "locale", value: "en", url: base }]);
|
|
35
|
+
await ready(page, "/login");
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
});
|
|
37
|
+
await expect(page.locator("html")).toHaveAttribute("lang", "en");
|
|
38
|
+
await expect(page.getByRole("button", { name: "Sign in", exact: true })).toBeVisible();
|
|
39
|
+
} finally {
|
|
40
|
+
await admin.put("/api/site/settings", { data: { locale: previous.locale ?? "en" } });
|
|
41
|
+
await admin.dispose();
|
|
42
|
+
}
|
|
47
43
|
});
|
|
48
44
|
|
|
49
45
|
test("managed admin labels react to locale changes", async ({ browser }) => {
|
|
@@ -13,9 +13,12 @@ test("admin can impersonate a user and stop", async ({ page }) => {
|
|
|
13
13
|
await row.getByRole("button").click();
|
|
14
14
|
await page.getByRole("menuitem", { name: "Impersonate" }).click();
|
|
15
15
|
|
|
16
|
-
// Impersonating a normal user leaves the admin-only route group
|
|
17
|
-
//
|
|
18
|
-
|
|
16
|
+
// Impersonating a normal user leaves the admin-only route group. Where the
|
|
17
|
+
// application-owned users page lands is the application's choice; the managed
|
|
18
|
+
// account page always offers the route back to the administrator session.
|
|
19
|
+
await expect(page).not.toHaveURL(/\/admin\/users/);
|
|
20
|
+
await ready(page, "/account");
|
|
21
|
+
await expect(page.getByText(`Impersonating ${USER.email}`)).toBeVisible();
|
|
19
22
|
const stop = page.getByRole("button", { name: "Stop impersonating" });
|
|
20
23
|
await expect(stop).toBeVisible();
|
|
21
24
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
2
|
import { anonState } from "../helpers/accounts";
|
|
3
3
|
import { ready } from "../helpers/hydration";
|
|
4
|
-
import { mailpitReachable,
|
|
4
|
+
import { mailpitReachable, waitForOtp } from "../helpers/mailpit";
|
|
5
5
|
|
|
6
6
|
test.use({ storageState: anonState });
|
|
7
7
|
|
|
@@ -26,10 +26,9 @@ test("login: an emailed one-time code signs a user in", async ({ page, playwrigh
|
|
|
26
26
|
test.skip(!caps?.emailOtp, "email otp disabled");
|
|
27
27
|
test.skip(!(await mailpitReachable()), "mailpit unreachable");
|
|
28
28
|
|
|
29
|
-
const email = `otp-${Date.now()}@example.com`;
|
|
29
|
+
const email = `ui-otp-${Date.now()}@example.com`;
|
|
30
30
|
await api.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "OTP" } });
|
|
31
31
|
await api.dispose();
|
|
32
|
-
await clearMailpit();
|
|
33
32
|
|
|
34
33
|
await ready(page, "/login");
|
|
35
34
|
await page.locator("#email").fill(email);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { expect, test, type Page } from "@playwright/test";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import { exclusive } from "../helpers/exclusive";
|
|
4
3
|
import { ready } from "../helpers/hydration";
|
|
5
4
|
|
|
6
5
|
// admin storageState (project default) — the admin is the resource owner.
|
|
@@ -14,10 +13,9 @@ const REDIRECT = "https://example.com/callback";
|
|
|
14
13
|
// (with PKCE) that validates the client and hands off to the consent page. The
|
|
15
14
|
// back-channel token exchange is driven by an OIDC client SDK / real RP — see
|
|
16
15
|
// docs/modules.md for the manual round-trip checklist.
|
|
17
|
-
test("OIDC provider: discovery, JWKS, client registration, authorize handoff", async ({ page, baseURL }) => {
|
|
18
|
-
// tests/api/auth.api.spec.ts also toggles the provider; the lock keeps the two apart.
|
|
16
|
+
test("OIDC provider: discovery, JWKS, client registration, authorize handoff", { tag: "@exclusive" }, async ({ page, baseURL }) => {
|
|
19
17
|
test.slow();
|
|
20
|
-
await
|
|
18
|
+
await frontChannel(page, baseURL);
|
|
21
19
|
});
|
|
22
20
|
|
|
23
21
|
async function frontChannel(page: Page, baseURL: string | undefined): Promise<void> {
|