diff --git a/.dockerignore b/.dockerignore index 7f15c4204..9269854cc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,4 +6,5 @@ README.md .next .git dev -.build \ No newline at end of file +.build +e2e \ No newline at end of file diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index a93cfe2f2..92efa0d03 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -91,6 +91,8 @@ jobs: network: host env: SKIP_ENV_VALIDATION: true + - name: Install playwright browsers + run: pnpm exec playwright install chromium - name: Run E2E Tests shell: bash run: pnpm test:e2e diff --git a/.gitignore b/.gitignore index bdc55c860..7e1d14c68 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,8 @@ apps/websocket/wssServer.cjs apps/nextjs/.million/ packages/cli/cli.cjs +# e2e mounts +e2e/shared/tmp #personal backgrounds apps/nextjs/public/images/background.png \ No newline at end of file diff --git a/e2e/lldap.spec.ts b/e2e/lldap.spec.ts new file mode 100644 index 000000000..f9015af55 --- /dev/null +++ b/e2e/lldap.spec.ts @@ -0,0 +1,92 @@ +import { chromium } from "playwright"; +import { GenericContainer } from "testcontainers"; +import { describe, expect, test } from "vitest"; + +import { OnboardingActions } from "./shared/actions/onboarding-actions"; +import { createHomarrContainer, withLogs } from "./shared/create-homarr-container"; +import { createSqliteDbFileAsync } from "./shared/e2e-db"; + +const defaultCredentials = { + username: "admin", + password: "password", + email: "admin@homarr.dev", + group: "lldap_admin", +}; + +const ldapBase = "dc=example,dc=com"; + +describe("LLDAP authorization", () => { + test("Authorize with LLDAP successfully", async () => { + // Arrange + const lldapContainer = await createLldapContainer().start(); + const { db, localMountPath } = await createSqliteDbFileAsync(); + const homarrContainer = await createHomarrContainer({ + environment: { + AUTH_PROVIDERS: "ldap", + AUTH_LDAP_URI: `ldap://host.docker.internal:${lldapContainer.getMappedPort(3890)}`, + AUTH_LDAP_BASE: ldapBase, + AUTH_LDAP_BIND_DN: `uid=${defaultCredentials.username},ou=People,${ldapBase}`, + AUTH_LDAP_BIND_PASSWORD: defaultCredentials.password, + }, + mounts: { + "/appdata": localMountPath, + }, + }).start(); + + const browser = await chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + + const onboardingActions = new OnboardingActions(page, db); + await onboardingActions.skipOnboardingAsync({ + group: defaultCredentials.group, + }); + + // Act + await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}/auth/login`); + await page.getByLabel("Username").fill(defaultCredentials.username); + await page.getByLabel("Password").fill(defaultCredentials.password); + await page.locator("css=button[type='submit']").click(); + + // Assert + await page.waitForURL(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`); + const users = await db.query.users.findMany({ + with: { + groups: { + with: { + group: true, + }, + }, + }, + }); + expect(users).toHaveLength(1); + const user = users[0]!; + expect(user).toEqual( + expect.objectContaining({ + name: defaultCredentials.username, + email: defaultCredentials.email, + provider: "ldap", + }), + ); + + const groups = user.groups.map((g) => g.group.name); + expect(groups).toContain(defaultCredentials.group); + + // Cleanup + await browser.close(); + await homarrContainer.stop(); + await lldapContainer.stop(); + }, 120_000); +}); + +const createLldapContainer = () => { + return withLogs( + new GenericContainer("lldap/lldap:stable").withExposedPorts(3890).withEnvironment({ + LLDAP_JWT_SECRET: "REPLACE_WITH_RANDOM", + LLDAP_KEY_SEED: "REPLACE_WITH_RANDOM", + LLDAP_LDAP_BASE_DN: ldapBase, + LLDAP_LDAP_USER_PASS: defaultCredentials.password, + LLDAP_LDAP_USER_EMAIL: defaultCredentials.email, + }), + ); +}; diff --git a/e2e/onboarding.spec.ts b/e2e/onboarding.spec.ts new file mode 100644 index 000000000..061f5e6d8 --- /dev/null +++ b/e2e/onboarding.spec.ts @@ -0,0 +1,85 @@ +import { chromium } from "playwright"; +import { describe, test } from "vitest"; + +import { OnboardingActions } from "./shared/actions/onboarding-actions"; +import { OnboardingAssertions } from "./shared/assertions/onboarding-assertions"; +import { createHomarrContainer } from "./shared/create-homarr-container"; +import { createSqliteDbFileAsync } from "./shared/e2e-db"; + +describe("Onboarding", () => { + test("Credentials onboarding should be successful", async () => { + // Arrange + const { db, localMountPath } = await createSqliteDbFileAsync(); + const homarrContainer = await createHomarrContainer({ + mounts: { + "/appdata": localMountPath, + }, + }).start(); + + const browser = await chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + const actions = new OnboardingActions(page, db); + const assertions = new OnboardingAssertions(page, db); + + // Act + await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`); + await actions.startOnboardingAsync("scratch"); + await actions.processUserStepAsync({ + username: "admin", + password: "Comp(exP4sswOrd", + confirmPassword: "Comp(exP4sswOrd", + }); + await actions.processSettingsStepAsync(); + + // Assert + await assertions.assertFinishStepVisibleAsync(); + await assertions.assertUserAndAdminGroupInsertedAsync("admin"); + await assertions.assertDbOnboardingStepAsync("finish"); + + // Cleanup + await browser.close(); + await homarrContainer.stop(); + }, 60_000); + + test("External provider onboarding setup should be successful", async () => { + // Arrange + const { db, localMountPath } = await createSqliteDbFileAsync(); + const homarrContainer = await createHomarrContainer({ + environment: { + AUTH_PROVIDERS: "ldap", + AUTH_LDAP_URI: "ldap://host.docker.internal:3890", + AUTH_LDAP_BASE: "", + AUTH_LDAP_BIND_DN: "", + AUTH_LDAP_BIND_PASSWORD: "", + }, + mounts: { + "/appdata": localMountPath, + }, + }).start(); + const externalGroupName = "oidc-admins"; + + const browser = await chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + const actions = new OnboardingActions(page, db); + const assertions = new OnboardingAssertions(page, db); + + // Act + await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`); + await actions.startOnboardingAsync("scratch"); + await actions.processExternalGroupStepAsync({ + name: externalGroupName, + }); + await actions.processSettingsStepAsync(); + + // Assert + await assertions.assertFinishStepVisibleAsync(); + await assertions.assertExternalGroupInsertedAsync(externalGroupName); + await assertions.assertDbOnboardingStepAsync("finish"); + + // Cleanup + await browser.close(); + await homarrContainer.stop(); + }, 60_000); +}); diff --git a/e2e/shared/actions/onboarding-actions.ts b/e2e/shared/actions/onboarding-actions.ts new file mode 100644 index 000000000..8362a46a5 --- /dev/null +++ b/e2e/shared/actions/onboarding-actions.ts @@ -0,0 +1,53 @@ +import { createId } from "@paralleldrive/cuid2"; +import type { Page } from "playwright"; + +import * as sqliteSchema from "../../../packages/db/schema/sqlite"; +import type { SqliteDatabase } from "../e2e-db"; + +export class OnboardingActions { + private readonly page: Page; + private readonly db: SqliteDatabase; + + constructor(page: Page, db: SqliteDatabase) { + this.page = page; + this.db = db; + } + + public async skipOnboardingAsync(input?: { group?: string }) { + await this.db.update(sqliteSchema.onboarding).set({ + step: "finish", + }); + + if (input?.group) { + await this.db.insert(sqliteSchema.groups).values({ + id: createId(), + name: input.group, + }); + } + } + + public async startOnboardingAsync(type: "scratch" | "before 1.0") { + await this.page.locator("button", { hasText: type }).click(); + } + + public async processUserStepAsync(input: { username: string; password: string; confirmPassword: string }) { + await this.page.waitForSelector("text=administrator user"); + + await this.page.getByLabel("Username").fill(input.username); + await this.page.getByLabel("Password", { exact: true }).fill(input.password); + await this.page.getByLabel("Confirm password").fill(input.confirmPassword); + + await this.page.locator("css=button[type='submit']").click(); + } + + public async processExternalGroupStepAsync(input: { name: string }) { + await this.page.waitForSelector("text=external provider"); + await this.page.locator("input").fill(input.name); + await this.page.locator("css=button[type='submit']").click(); + } + + public async processSettingsStepAsync() { + await this.page.waitForSelector("text=Analytics"); + await this.page.locator("css=button[type='submit']").click(); + } +} diff --git a/e2e/shared/assertions/onboarding-assertions.ts b/e2e/shared/assertions/onboarding-assertions.ts new file mode 100644 index 000000000..a2f9ccb2f --- /dev/null +++ b/e2e/shared/assertions/onboarding-assertions.ts @@ -0,0 +1,62 @@ +import { eq } from "drizzle-orm"; +import type { Page } from "playwright"; +import { expect } from "vitest"; + +import * as sqliteSchema from "../../../packages/db/schema/sqlite"; +import { OnboardingStep } from "../../../packages/definitions/src"; +import { credentialsAdminGroup } from "../../../packages/definitions/src/group"; +import type { SqliteDatabase } from "../e2e-db"; + +export class OnboardingAssertions { + private readonly page: Page; + private readonly db: SqliteDatabase; + + constructor(page: Page, db: SqliteDatabase) { + this.page = page; + this.db = db; + } + + public async assertDbOnboardingStepAsync(expectedStep: OnboardingStep) { + const onboarding = await this.db.query.onboarding.findFirst(); + expect(onboarding?.step).toEqual(expectedStep); + } + + public async assertUserAndAdminGroupInsertedAsync(expectedUsername: string) { + const users = await this.db.query.users.findMany({ + with: { + groups: { + with: { + group: { + with: { + permissions: true, + }, + }, + }, + }, + }, + }); + + const user = users.find((u) => u.name === expectedUsername); + expect(user).toBeDefined(); + + const adminGroup = user!.groups.find((g) => g.group.name === credentialsAdminGroup); + expect(adminGroup).toBeDefined(); + expect(adminGroup!.group.permissions).toEqual([expect.objectContaining({ permission: "admin" })]); + } + + public async assertExternalGroupInsertedAsync(expectedGroupName: string) { + const group = await this.db.query.groups.findFirst({ + where: eq(sqliteSchema.groups.name, expectedGroupName), + with: { + permissions: true, + }, + }); + + expect(group).toBeDefined(); + expect(group!.permissions).toEqual([expect.objectContaining({ permission: "admin" })]); + } + + public async assertFinishStepVisibleAsync() { + await this.page.waitForSelector("text=completed the setup", { timeout: 5000 }); + } +} diff --git a/e2e/shared/create-homarr-container.ts b/e2e/shared/create-homarr-container.ts index 3183e66ca..7e5bcd5c4 100644 --- a/e2e/shared/create-homarr-container.ts +++ b/e2e/shared/create-homarr-container.ts @@ -1,18 +1,43 @@ import { GenericContainer, Wait } from "testcontainers"; +import { Environment } from "testcontainers/build/types"; -export const createHomarrContainer = () => { +export const createHomarrContainer = ( + options: { + environment?: Environment; + mounts?: { + "/appdata"?: string; + "/var/run/docker.sock"?: string; + }; + } = {}, +) => { if (!process.env.CI) { throw new Error("This test should only be run in CI or with a homarr image named 'homarr-e2e'"); } - return withLogs( - new GenericContainer("homarr-e2e") - .withExposedPorts(7575) - .withEnvironment({ - SECRET_ENCRYPTION_KEY: "0".repeat(64), - }) - .withWaitStrategy(Wait.forHttp("/api/health/ready", 7575)), - ); + const container = new GenericContainer("homarr-e2e") + .withExposedPorts(7575) + .withEnvironment({ + ...options.environment, + SECRET_ENCRYPTION_KEY: "0".repeat(64), + }) + .withBindMounts( + Object.entries(options.mounts ?? {}) + .filter((item) => item?.[0] !== undefined) + .map(([container, local]) => ({ + source: local, + target: container, + })), + ) + .withWaitStrategy(Wait.forHttp("/api/health/ready", 7575)) + .withExtraHosts([ + { + // This enabled the usage of host.docker.internal as hostname in the container + host: "host.docker.internal", + ipAddress: "host-gateway", + }, + ]); + + return withLogs(container); }; export const withLogs = (container: GenericContainer) => { diff --git a/e2e/shared/e2e-db.ts b/e2e/shared/e2e-db.ts new file mode 100644 index 000000000..0abc87823 --- /dev/null +++ b/e2e/shared/e2e-db.ts @@ -0,0 +1,32 @@ +import { mkdir } from "fs/promises"; +import path from "path"; +import { createId } from "@paralleldrive/cuid2"; +import Database from "better-sqlite3"; +import { BetterSQLite3Database, drizzle } from "drizzle-orm/better-sqlite3"; +import { migrate } from "drizzle-orm/better-sqlite3/migrator"; + +import * as sqliteSchema from "../../packages/db/schema/sqlite"; + +export const createSqliteDbFileAsync = async () => { + const localMountPath = path.join(__dirname, "tmp", createId()); + await mkdir(path.join(localMountPath, "db"), { recursive: true }); + + const localDbUrl = path.join(localMountPath, "db", "db.sqlite"); + + const connection = new Database(localDbUrl); + const db = drizzle(connection, { + schema: sqliteSchema, + casing: "snake_case", + }); + + await migrate(db, { + migrationsFolder: path.join(__dirname, "..", "..", "packages", "db", "migrations", "sqlite"), + }); + + return { + db, + localMountPath, + }; +}; + +export type SqliteDatabase = BetterSQLite3Database; diff --git a/packages/definitions/src/docs/homarr-docs-sitemap.ts b/packages/definitions/src/docs/homarr-docs-sitemap.ts index 1ef500ee0..b733a9b67 100644 --- a/packages/definitions/src/docs/homarr-docs-sitemap.ts +++ b/packages/definitions/src/docs/homarr-docs-sitemap.ts @@ -9,6 +9,7 @@ export type HomarrDocumentationPath = | "/blog/2023/12/22/updated-documentation" | "/blog/2024/09/23/version-1.0" | "/blog/2024/12/17/open-beta-1.0" + | "/blog/2024/12/31/migrate-secret-enryption-key" | "/blog/archive" | "/blog/authors" | "/blog/authors/ajnart" @@ -100,11 +101,13 @@ export type HomarrDocumentationPath = | "/docs/tags/open-media-vault" | "/docs/tags/overseerr" | "/docs/tags/permissions" + | "/docs/tags/pgid" | "/docs/tags/pi-hole" | "/docs/tags/ping" | "/docs/tags/programming" | "/docs/tags/proxmox" | "/docs/tags/proxy" + | "/docs/tags/puid" | "/docs/tags/roles" | "/docs/tags/rss" | "/docs/tags/search" @@ -135,6 +138,7 @@ export type HomarrDocumentationPath = | "/docs/advanced/icons" | "/docs/advanced/keyboard-shortcuts" | "/docs/advanced/proxy" + | "/docs/advanced/running-as-different-user" | "/docs/advanced/single-sign-on" | "/docs/category/advanced" | "/docs/category/community" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8619d666..05bd7e0fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -183,13 +183,13 @@ importers: version: 5.62.12(@tanstack/react-query@5.62.12(react@19.0.0))(react@19.0.0) '@tanstack/react-query-next-experimental': specifier: 5.62.12 - version: 5.62.12(@tanstack/react-query@5.62.12(react@19.0.0))(next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0) + version: 5.62.12(@tanstack/react-query@5.62.12(react@19.0.0))(next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0) '@trpc/client': specifier: next version: 11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2) '@trpc/next': specifier: next - version: 11.0.0-rc.682(@tanstack/react-query@5.62.12(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.0.0-rc.682(@tanstack/react-query@5.62.12(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2) + version: 11.0.0-rc.682(@tanstack/react-query@5.62.12(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.0.0-rc.682(@tanstack/react-query@5.62.12(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2) '@trpc/react-query': specifier: next version: 11.0.0-rc.682(@tanstack/react-query@5.62.12(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2) @@ -231,7 +231,7 @@ importers: version: 2.0.0-beta.7(@mantine/core@7.15.2(@mantine/hooks@7.15.2(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.15.2(@mantine/core@7.15.2(@mantine/hooks@7.15.2(react@19.0.0))(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.2(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.2(react@19.0.0))(@tabler/icons-react@3.26.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) postcss-preset-mantine: specifier: ^1.17.0 version: 1.17.0(postcss@8.4.47) @@ -556,7 +556,7 @@ importers: version: 4.5.0 next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -626,10 +626,10 @@ importers: version: 7.3.0 next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) next-auth: specifier: 5.0.0-beta.25 - version: 5.0.0-beta.25(next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0) + version: 5.0.0-beta.25(next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -706,7 +706,7 @@ importers: version: 1.11.13 next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -1195,7 +1195,7 @@ importers: version: 1.11.13 next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -1290,7 +1290,7 @@ importers: version: 0.5.16 next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -1530,7 +1530,7 @@ importers: version: 2.11.0(@types/react@18.3.13)(react@19.0.0) next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -1573,10 +1573,10 @@ importers: version: 2.0.0-beta.7(@mantine/core@7.15.2(@mantine/hooks@7.15.2(react@19.0.0))(@types/react@18.3.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.15.2(@mantine/core@7.15.2(@mantine/hooks@7.15.2(react@19.0.0))(@types/react@18.3.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.2(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.2(react@19.0.0))(@tabler/icons-react@3.26.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) next-intl: specifier: 3.26.3 - version: 3.26.3(next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0) + version: 3.26.3(next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -1631,7 +1631,7 @@ importers: version: 2.0.0-beta.7(@mantine/core@7.15.2(@mantine/hooks@7.15.2(react@19.0.0))(@types/react@18.3.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.15.2(@mantine/core@7.15.2(@mantine/hooks@7.15.2(react@19.0.0))(@types/react@18.3.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.2(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.2(react@19.0.0))(@tabler/icons-react@3.26.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -1804,7 +1804,7 @@ importers: version: 2.0.0-beta.7(@mantine/core@7.15.2(@mantine/hooks@7.15.2(react@19.0.0))(@types/react@18.3.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.15.2(@mantine/core@7.15.2(@mantine/hooks@7.15.2(react@19.0.0))(@types/react@18.3.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.2(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.15.2(react@19.0.0))(@tabler/icons-react@3.26.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: ^14.2.22 - version: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + version: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -3341,6 +3341,11 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.49.1': + resolution: {integrity: sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==} + engines: {node: '>=18'} + hasBin: true + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -7577,11 +7582,21 @@ packages: engines: {node: '>=18'} hasBin: true + playwright-core@1.49.1: + resolution: {integrity: sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==} + engines: {node: '>=18'} + hasBin: true + playwright@1.49.0: resolution: {integrity: sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==} engines: {node: '>=18'} hasBin: true + playwright@1.49.1: + resolution: {integrity: sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==} + engines: {node: '>=18'} + hasBin: true + possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -10772,6 +10787,11 @@ snapshots: '@pkgr/core@0.1.1': {} + '@playwright/test@1.49.1': + dependencies: + playwright: 1.49.1 + optional: true + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -11335,10 +11355,10 @@ snapshots: '@tanstack/react-query': 5.62.12(react@19.0.0) react: 19.0.0 - '@tanstack/react-query-next-experimental@5.62.12(@tanstack/react-query@5.62.12(react@19.0.0))(next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0)': + '@tanstack/react-query-next-experimental@5.62.12(@tanstack/react-query@5.62.12(react@19.0.0))(next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0)': dependencies: '@tanstack/react-query': 5.62.12(react@19.0.0) - next: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + next: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: 19.0.0 '@tanstack/react-query@5.62.12(react@19.0.0)': @@ -11582,11 +11602,11 @@ snapshots: '@trpc/server': 11.0.0-rc.682(typescript@5.7.2) typescript: 5.7.2 - '@trpc/next@11.0.0-rc.682(@tanstack/react-query@5.62.12(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.0.0-rc.682(@tanstack/react-query@5.62.12(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2)': + '@trpc/next@11.0.0-rc.682(@tanstack/react-query@5.62.12(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.0.0-rc.682(@tanstack/react-query@5.62.12(react@19.0.0))(@trpc/client@11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2))(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2)': dependencies: '@trpc/client': 11.0.0-rc.682(@trpc/server@11.0.0-rc.682(typescript@5.7.2))(typescript@5.7.2) '@trpc/server': 11.0.0-rc.682(typescript@5.7.2) - next: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + next: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) typescript: 5.7.2 @@ -15219,21 +15239,21 @@ snapshots: netmask@2.0.2: {} - next-auth@5.0.0-beta.25(next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0): + next-auth@5.0.0-beta.25(next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0): dependencies: '@auth/core': 0.37.2 - next: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + next: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: 19.0.0 - next-intl@3.26.3(next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0): + next-intl@3.26.3(next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0))(react@19.0.0): dependencies: '@formatjs/intl-localematcher': 0.5.5 negotiator: 1.0.0 - next: 14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) + next: 14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0) react: 19.0.0 use-intl: 3.26.3(react@19.0.0) - next@14.2.22(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0): + next@14.2.22(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.83.0): dependencies: '@next/env': 14.2.22 '@swc/helpers': 0.5.5 @@ -15254,6 +15274,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 14.2.22 '@next/swc-win32-ia32-msvc': 14.2.22 '@next/swc-win32-x64-msvc': 14.2.22 + '@playwright/test': 1.49.1 sass: 1.83.0 transitivePeerDependencies: - '@babel/core' @@ -15687,12 +15708,22 @@ snapshots: playwright-core@1.49.0: {} + playwright-core@1.49.1: + optional: true + playwright@1.49.0: dependencies: playwright-core: 1.49.0 optionalDependencies: fsevents: 2.3.2 + playwright@1.49.1: + dependencies: + playwright-core: 1.49.1 + optionalDependencies: + fsevents: 2.3.2 + optional: true + possible-typed-array-names@1.0.0: {} postcss-js@4.0.1(postcss@8.4.47):