@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.
Files changed (211) hide show
  1. package/README.md +2 -1
  2. package/dist/app-proxy-timeouts.d.ts +27 -0
  3. package/dist/app-proxy-timeouts.js +180 -0
  4. package/dist/assemble.d.ts +5 -2
  5. package/dist/assemble.js +14 -4
  6. package/dist/deploy-checks.d.ts +44 -0
  7. package/dist/deploy-checks.js +267 -0
  8. package/dist/deploy-compose-profile.d.ts +6 -2
  9. package/dist/deploy-compose-profile.js +15 -6
  10. package/dist/deploy-compose-render.d.ts +12 -4
  11. package/dist/deploy-compose-render.js +47 -18
  12. package/dist/deploy-compose.d.ts +2 -5
  13. package/dist/deploy-compose.js +7 -2
  14. package/dist/deploy-profile.d.ts +6 -2
  15. package/dist/deploy-profile.js +11 -7
  16. package/dist/deploy-render.d.ts +4 -1
  17. package/dist/deploy-render.js +27 -21
  18. package/dist/deploy-schema.d.ts +56 -2
  19. package/dist/deploy-schema.js +130 -14
  20. package/dist/deploy.d.ts +2 -5
  21. package/dist/deploy.js +5 -2
  22. package/dist/dev.d.ts +21 -0
  23. package/dist/dev.js +78 -3
  24. package/dist/eject.js +2 -0
  25. package/dist/index.js +85 -5
  26. package/dist/lib.d.ts +2 -0
  27. package/dist/lib.js +5 -2
  28. package/dist/lockfile.d.ts +24 -0
  29. package/dist/lockfile.js +74 -4
  30. package/dist/provider.js +10 -5
  31. package/dist/remove.js +1 -1
  32. package/dist/template-files.d.ts +42 -0
  33. package/dist/template-files.js +102 -0
  34. package/dist/templates/fullstack/AGENTS.md +28 -4
  35. package/dist/templates/fullstack/README.md +8 -7
  36. package/dist/templates/fullstack/apps/api/package.json +4 -4
  37. package/dist/templates/fullstack/apps/api/scripts/build.extensions.mjs +6 -0
  38. package/dist/templates/fullstack/apps/api/scripts/build.mjs +65 -16
  39. package/dist/templates/fullstack/apps/api/scripts/run-migrations.mjs +7 -3
  40. package/dist/templates/fullstack/apps/api/scripts/verify-api-contract.ts +5 -1
  41. package/dist/templates/fullstack/apps/api/src/app.spec.ts +22 -0
  42. package/dist/templates/fullstack/apps/api/src/app.ts +41 -25
  43. package/dist/templates/fullstack/apps/api/src/build.spec.ts +118 -0
  44. package/dist/templates/fullstack/apps/api/src/config/env.extensions.ts +20 -0
  45. package/dist/templates/fullstack/apps/api/src/config/env.validation.spec.ts +76 -0
  46. package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +18 -3
  47. package/dist/templates/fullstack/apps/api/src/config/provider-selection.spec.ts +53 -0
  48. package/dist/templates/fullstack/apps/api/src/config/provider-selection.ts +79 -0
  49. package/dist/templates/fullstack/apps/api/src/config/providers.ts +7 -3
  50. package/dist/templates/fullstack/apps/api/src/core/api-contract.extensions.ts +6 -0
  51. package/dist/templates/fullstack/apps/api/src/core/api-contract.spec.ts +29 -0
  52. package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +39 -2
  53. package/dist/templates/fullstack/apps/api/src/core/services.ts +13 -0
  54. package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +27 -0
  55. package/dist/templates/fullstack/apps/api/src/database/data-source.ts +7 -10
  56. package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +12 -0
  57. package/dist/templates/fullstack/apps/api/src/database/database.ts +2 -3
  58. package/dist/templates/fullstack/apps/api/src/database/entities.ts +12 -0
  59. package/dist/templates/fullstack/apps/api/src/database/migrate.ts +12 -2
  60. package/dist/templates/fullstack/apps/api/src/database/migration-files.spec.ts +116 -0
  61. package/dist/templates/fullstack/apps/api/src/database/migration-files.ts +134 -0
  62. package/dist/templates/fullstack/apps/api/src/database/migration-readiness.spec.ts +36 -0
  63. package/dist/templates/fullstack/apps/api/src/database/migration-readiness.ts +25 -0
  64. package/dist/templates/fullstack/apps/api/src/database/run-migrations.spec.ts +12 -1
  65. package/dist/templates/fullstack/apps/api/src/database/sqlite-migrator.ts +2 -36
  66. package/dist/templates/fullstack/apps/api/src/main.ts +2 -2
  67. package/dist/templates/fullstack/apps/api/tsconfig.json +2 -0
  68. package/dist/templates/fullstack/apps/web/package.json +1 -1
  69. package/dist/templates/fullstack/apps/web/src/app.css +3 -0
  70. package/dist/templates/fullstack/apps/web/src/app.extensions.css +11 -0
  71. package/dist/templates/fullstack/apps/web/src/lib/components/site-runtime.svelte +3 -1
  72. package/dist/templates/fullstack/apps/web/src/lib/server/api.test.ts +19 -0
  73. package/dist/templates/fullstack/apps/web/src/lib/server/api.ts +4 -1
  74. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +79 -0
  75. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +55 -1
  76. package/dist/templates/fullstack/apps/web/src/lib/server/proxy-timeouts.ts +2 -1
  77. package/dist/templates/fullstack/apps/web/tsconfig.json +1 -1
  78. package/dist/templates/fullstack/apps/web/vite.config.test.ts +33 -0
  79. package/dist/templates/fullstack/apps/web/vite.config.ts +55 -13
  80. package/dist/templates/fullstack/apps/web/vite.extensions.ts +23 -0
  81. package/dist/templates/fullstack/compose.dev.yaml +7 -4
  82. package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/SKILL.md +2 -2
  83. package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +12 -3
  84. package/dist/templates/fullstack/dot-claude/skills/podokit-update/SKILL.md +5 -2
  85. package/dist/templates/fullstack/dot-env.docker.example +5 -2
  86. package/dist/templates/fullstack/dot-env.example +4 -0
  87. package/dist/templates/fullstack/dot-podokit/dev.json +1 -1
  88. package/dist/templates/fullstack/infra/k3s/configmap.yaml +1 -0
  89. package/dist/templates/fullstack/tests/README.md +40 -1
  90. package/dist/templates/fullstack/tests/helpers/app-paths.ts +18 -0
  91. package/dist/templates/fullstack/tests/helpers/paths.ts +33 -0
  92. package/dist/templates/fullstack/tests/package.json +2 -2
  93. package/dist/templates/fullstack/tests/playwright.config.ts +7 -3
  94. package/dist/templates/fullstack/tests/playwright.extensions.ts +88 -1
  95. package/dist/templates/fullstack/tests/ui/smoke.ui.spec.ts +5 -4
  96. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +37 -1
  97. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +15 -6
  98. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.test.ts +31 -0
  99. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.ts +31 -0
  100. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flags.ts +12 -0
  101. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-nav.ts +11 -0
  102. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.test.ts +39 -0
  103. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.ts +35 -0
  104. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +15 -0
  105. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +11 -1
  106. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/admin-sidebar.svelte +2 -23
  107. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +7 -2
  108. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.test.ts +30 -0
  109. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/impersonation-banner.svelte +3 -2
  110. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/settings/app-flags-settings.svelte +65 -0
  111. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +32 -23
  112. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.test.ts +27 -0
  113. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-footer.svelte +4 -1
  114. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +3 -1
  115. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -0
  116. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -0
  117. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/app-hooks.ts +11 -0
  118. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/non-indexable-paths.ts +8 -0
  119. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.test.ts +31 -0
  120. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.ts +5 -1
  121. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +1 -1
  122. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts +2 -1
  123. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.svelte +1 -5
  124. package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -0
  125. package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +1 -1
  126. package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +159 -170
  127. package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +72 -81
  128. package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +1 -6
  129. package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +1 -1
  130. package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +43 -53
  131. package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +3 -1
  132. package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +24 -6
  133. package/dist/templates/modules/admin-dashboard/files/tests/helpers/app-accounts.ts +24 -0
  134. package/dist/templates/modules/admin-dashboard/files/tests/helpers/devices.ts +41 -11
  135. package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -3
  136. package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +3 -9
  137. package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +2 -4
  138. package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +11 -5
  139. package/dist/templates/modules/admin-dashboard/files/tests/ui/account-flows.ui.spec.ts +4 -5
  140. package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +55 -55
  141. package/dist/templates/modules/admin-dashboard/files/tests/ui/app-flags.ui.spec.ts +63 -0
  142. package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +12 -16
  143. package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +3 -2
  144. package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +32 -35
  145. package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +4 -2
  146. package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +1 -2
  147. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -2
  148. package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +82 -91
  149. package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +6 -2
  150. package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +16 -20
  151. package/dist/templates/modules/admin-dashboard/files/tests/ui/impersonation.ui.spec.ts +6 -3
  152. package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +2 -3
  153. package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +2 -4
  154. package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +5 -2
  155. package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +7 -2
  156. package/dist/templates/modules/admin-dashboard/files/tests/ui/same-origin-requests.ui.spec.ts +89 -0
  157. package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +5 -4
  158. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +67 -73
  159. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +84 -93
  160. package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +8 -7
  161. package/dist/templates/modules/admin-dashboard/files/tests/ui/theme-mode.ui.spec.ts +29 -0
  162. package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +51 -54
  163. package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +10 -9
  164. package/dist/templates/modules/admin-dashboard/module.manifest.json +7 -2
  165. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +5 -1
  166. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +17 -0
  167. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +18 -0
  168. package/dist/templates/modules/audit-log/module.manifest.json +15 -3
  169. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-app-name.spec.ts +21 -0
  170. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +13 -1
  171. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-environment.ts +9 -2
  172. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +19 -3
  173. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +2 -0
  174. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.runtime.spec.ts +10 -0
  175. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +7 -5
  176. package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.spec.ts +86 -0
  177. package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.ts +44 -0
  178. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +2 -1
  179. package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +3 -1
  180. package/dist/templates/modules/auth/files/apps/api/src/settings/app-flags.ts +9 -0
  181. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.spec.ts +43 -0
  182. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +30 -5
  183. package/dist/templates/modules/auth/module.manifest.json +8 -2
  184. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +57 -1
  185. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +93 -9
  186. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.spec.ts +40 -0
  187. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +52 -5
  188. package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +9 -7
  189. package/dist/templates/modules/bullmq/files/apps/api/src/worker.extensions.ts +13 -0
  190. package/dist/templates/modules/bullmq/module.manifest.json +5 -1
  191. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +2 -0
  192. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +87 -1
  193. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +93 -3
  194. package/dist/templates/modules/jobs-local/module.manifest.json +3 -0
  195. package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.spec.ts +16 -0
  196. package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.ts +15 -10
  197. package/dist/templates/modules/mailer/module.manifest.json +1 -1
  198. package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +1 -1
  199. package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -4
  200. package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +1 -1
  201. package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +45 -20
  202. package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +1 -1
  203. package/dist/templates/todo/AGENTS.md +5 -1
  204. package/dist/templates/todo/README.md +6 -4
  205. package/dist/text-diff.d.ts +12 -0
  206. package/dist/text-diff.js +111 -0
  207. package/dist/update.d.ts +61 -3
  208. package/dist/update.js +152 -27
  209. package/package.json +2 -2
  210. package/dist/templates/modules/admin-dashboard/files/tests/api/exclusive.api.spec.ts +0 -75
  211. 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
- test.slow();
54
- await exclusive("site-theme", async () => {
55
- await openAppearance(page);
56
- await page.getByRole("button", { name: "Slate" }).click();
57
- await page.getByRole("button", { name: "Medium" }).click();
58
- await page.getByRole("button", { name: "Save changes" }).click();
59
- await expect(page.getByText("General settings saved.").first()).toBeVisible();
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
- // "Restore defaults" reverts everything in one click and persists.
64
- await page.getByRole("button", { name: "Restore defaults" }).click();
65
- await expect(page.getByText("General settings saved.").first()).toBeVisible();
66
- await expect.poll(async () => page.locator("#podokit-theme").count()).toBe(0);
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("admin@example.com");
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("admin@example.com").first()).toBeVisible();
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
- await exclusive("social-providers", async () => {
72
- const admin = await playwright.request.newContext({
73
- baseURL: base,
74
- extraHTTPHeaders: { origin: base },
75
- });
76
- await admin.post("/api/auth/sign-in/email", {
77
- data: { email: ADMIN.email, password: ADMIN.password },
78
- });
79
- try {
80
- expect(
81
- (await admin.put("/api/account/auth-config", {
82
- data: {
83
- social: {
84
- google: {
85
- enabled: true,
86
- clientId: "dummy-google-client-id",
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
- })).ok(),
92
- ).toBeTruthy();
93
- await expect
94
- .poll(async () => {
95
- await page.goto("/login");
96
- return page.getByRole("button", { name: "Continue with Google" }).count();
97
- }, { timeout: 8_000 })
98
- .toBe(1);
99
- } finally {
100
- await admin.put("/api/account/auth-config", {
101
- data: { social: { google: { enabled: false } } },
102
- });
103
- await admin.dispose();
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
- await ready(page, "/");
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.getByRole("button", { name: /admin@example.com/ }).click();
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 { clearMailpit, mailpitReachable, waitForLink } from "../helpers/mailpit";
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 { clearMailpit, mailpitReachable, waitForLink } from "../helpers/mailpit";
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 { exclusive } from "../helpers/exclusive";
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. The site-general lock keeps other
9
- // specs that save these settings out while one runs, also with --workers above
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
- test.slow();
35
- await exclusive("site-general", async () => {
36
- await ready(page, "/admin/settings");
37
- const origin = new URL(page.url()).origin;
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
- try {
42
- expect((await page.request.put(settingsUrl, { data: { locale: "" } })).ok()).toBeTruthy();
43
- await ready(page, "/admin/settings");
44
- await expect(page.locator("#locale")).toContainText("English");
45
- } finally {
46
- await page.request.put(settingsUrl, { data: { locale: previous.locale ?? "" } });
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 exclusive("site-general", async () => {
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 footer.fill("© 2026 PodoKit Test");
64
- await support.fill("help@example.com");
76
+ await page.getByLabel("Footer text").fill(origFooter);
77
+ await page.getByLabel("Support email").fill(origSupport);
65
78
  },
66
- { footerText: "© 2026 PodoKit Test", supportEmail: "help@example.com" },
79
+ { footerText: origFooter, supportEmail: origSupport },
67
80
  );
68
- try {
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 exclusive("site-general", async () => {
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 origin = new URL(page.url()).origin;
94
- await page.getByRole("switch", { name: "Maintenance mode" }).click();
95
- await saveGeneral(page);
96
- try {
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 exclusive("site-general", async () => {
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 origin = new URL(page.url()).origin;
121
- await page.getByRole("switch", { name: "Allow public sign-up" }).click();
122
- await saveGeneral(page);
123
- try {
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
- const landing = await request.get("/");
20
- expect(landing.headers()["x-robots-tag"]).toBeUndefined();
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
- test.slow();
28
- await exclusive("site-general", async () => {
29
- const admin = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
30
- await admin.post("/api/auth/sign-in/email", {
31
- data: { email: ADMIN.email, password: ADMIN.password },
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
- try {
36
- expect((await admin.put("/api/site/settings", { data: { locale: "ko" } })).ok()).toBeTruthy();
37
- await page.context().addCookies([{ name: "locale", value: "en", url: base }]);
38
- await ready(page, "/login");
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
- await expect(page.locator("html")).toHaveAttribute("lang", "en");
41
- await expect(page.getByRole("button", { name: "Sign in", exact: true })).toBeVisible();
42
- } finally {
43
- await admin.put("/api/site/settings", { data: { locale: previous.locale ?? "en" } });
44
- await admin.dispose();
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 and keeps a
17
- // visible route back to the original administrator session.
18
- await expect(page).toHaveURL(/\/account(\/|$)/);
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, clearMailpit, waitForOtp } from "../helpers/mailpit";
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 exclusive("oidc-provider", () => frontChannel(page, baseURL));
18
+ await frontChannel(page, baseURL);
21
19
  });
22
20
 
23
21
  async function frontChannel(page: Page, baseURL: string | undefined): Promise<void> {