diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8b792301c..fa2ffdaef 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -252,3 +252,155 @@ jobs: files: ./fdm-data/coverage/coverage-final.json flags: fdm-data token: ${{ secrets.CODECOV_TOKEN }} + + test-app: + name: app + # Containers must run in Linux based operating systems + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [22] + permissions: + contents: read + packages: write + # Docker Hub image that `container-job` executes in + container: node:${{ matrix.node-version }}-bookworm-slim + + # Service containers to run with `container-job` + services: + # Label used to access the service container + postgres: + # Docker Hub image with postgis extension + image: postgis/postgis:17-3.5 + # Provide the password for postgres + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + # The hostname used to communicate with the PostgreSQL service container + POSTGRES_HOST: postgres + # The default PostgreSQL port + POSTGRES_PORT: 5432 + # the default usernam + POSTGRES_USER: postgres + # the default password + POSTGRES_PASSWORD: postgres + # the default database + POSTGRES_DB: postgres + + # Public name of the application displayed in the UI. + PUBLIC_FDM_NAME: MINAS4 + # Base URL of the application (used for API calls, etc.). + PUBLIC_FDM_URL: http://localhost:3000 + # URL to the privacy policy document. + PUBLIC_FDM_PRIVACY_URL: http://localhost:3000/privacy + # Secret key used to sign session cookies. MUST be a strong, random string. + FDM_SESSION_SECRET: blablabla1 + # Authentication (Better Auth & OAuth Providers) + BETTER_AUTH_SECRET: blablabla2 + # Full base URL of this application (used for redirects by better-auth). + BETTER_AUTH_URL: http://localhost:3000 + # Mapbox API token for displaying maps. + PUBLIC_MAPBOX_TOKEN: ${{ secrets.PUBLIC_MAPBOX_TOKEN }} + # URL to the FlatGeobuf (.fgb) file containing selectable field geometries. + AVAILABLE_FIELDS_URL: https://storage.googleapis.com/fdm-public-data/fields/nl/2024/draft.fgb + steps: + # Include dependencies for codecov + - name: Install system dependencies + run: apt-get update && apt-get install -y git curl gpg + + # Downloads a copy of the code in your repository before running CI tests + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Cache turbo build setup + uses: actions/cache@v4 + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo- + + - name: Setup pnpm 10 + uses: pnpm/action-setup@v4 + with: + version: 10.14.0 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + registry-url: 'https://npm.pkg.github.com' + cache: 'pnpm' + + - name: Install Dependencies + run: pnpm i + + - name: Install Playwright browsers + run: pnpm exec playwright install --with-deps + working-directory: ./fdm-app + + - name: Build fdm-data + run: pnpm build + working-directory: ./fdm-data + + - name: Build fdm-core + run: pnpm build + working-directory: ./fdm-core + + - name: Build fdm-calculator + run: pnpm build + working-directory: ./fdm-calculator + + - name: Build fdm-app + run: pnpm build + working-directory: ./fdm-app + + - name: Migrate database + run: pnpm db:migrate + working-directory: ./fdm-app + + - name: Run login tests with coverage + run: pnpm test-login-ci + working-directory: ./fdm-app + + - name: Move client coverage report for login tests + run: mv coverage/coverage-final.json coverage/coverage-final-login.json + working-directory: ./fdm-app + + - name: Compile server coverage report + run: pnpm coverage-report-ci + working-directory: ./fdm-app + + - name: Move server coverage report for login tests + run: mv coverage/coverage-final.json coverage/coverage-final-login-server.json + working-directory: ./fdm-app + + - name: Run app tests with coverage + run: pnpm test-ci + working-directory: ./fdm-app + + - name: Move client coverage report for app tests + run: mv coverage/coverage-final.json coverage/coverage-final-app.json + working-directory: ./fdm-app + + - name: Compile server coverage report + run: pnpm coverage-report-ci + working-directory: ./fdm-app + + - name: Move server coverage report for app tests + run: mv coverage/coverage-final.json coverage/coverage-final-app-server.json + working-directory: ./fdm-app + + - name: fdm-app - Upload results to Codecov + uses: codecov/codecov-action@v5 + with: + files: ./fdm-app/coverage/coverage-final*.json + flags: fdm-app + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/fdm-app/.gitignore b/fdm-app/.gitignore index f99244234..cfb131f96 100644 --- a/fdm-app/.gitignore +++ b/fdm-app/.gitignore @@ -4,8 +4,17 @@ node_modules /build /uploads .env +.env.test .env.production +dist +dist-ssr +playwright-report +test-results +test-tmp +coverage +*.local + .react-router/ # Sentry Config File .env.sentry-build-plugin diff --git a/fdm-app/app/lib/email.server.ts b/fdm-app/app/lib/email.server.ts index 3598b83d1..59e0d3991 100644 --- a/fdm-app/app/lib/email.server.ts +++ b/fdm-app/app/lib/email.server.ts @@ -11,6 +11,12 @@ import { serverConfig } from "~/lib/config.server" import type { ExtendedUser } from "~/types/extended-user" const client = new postmark.ServerClient(String(process.env.POSTMARK_API_KEY)) +const writeMagicLinkFile = + (process.env.CI && process.env.CI.length > 0) || + (process.env.WRITE_MAGIC_LINK_FILE && + process.env.WRITE_MAGIC_LINK_FILE.length > 0) +const sendRealEmail = + process.env.POSTMARK_API_KEY && process.env.POSTMARK_API_KEY.length > 0 interface Email { From: string @@ -141,7 +147,9 @@ function getTimeZoneFromUrl(url: string): string | undefined { } export async function sendEmail(email: Email): Promise { - await client.sendEmail(email) + if (sendRealEmail) { + await client.sendEmail(email) + } } // Helper function to send magic link emails, to be passed to fdm-core @@ -149,6 +157,17 @@ export async function sendMagicLinkEmailToUser( emailAddress: string, magicLinkUrl: string, ): Promise { + if (writeMagicLinkFile) { + const testIo = await import("@/tests/test-io") + await testIo.writeTestFileLine( + testIo.magicLinkUrlFileName, + magicLinkUrl, + { + tmpUrl: testIo.runtimeTestTmpUrl(), + }, + ) + } + const email = await renderMagicLinkEmail(emailAddress, magicLinkUrl) await sendEmail(email) } diff --git a/fdm-app/package.json b/fdm-app/package.json index fa98e58c9..562725acd 100644 --- a/fdm-app/package.json +++ b/fdm-app/package.json @@ -10,6 +10,10 @@ "dotenvx": "dotenvx", "start": "pnpm db:migrate && react-router-serve ./build/server/index.js", "start-dev": "dotenvx run -- pnpm db:migrate && dotenvx run -- react-router-serve ./build/server/index.js", + "test": "dotenvx run -- playwright test -c ./playwright-login.config.ts & dotenvx run -- playwright test -c ./playwright-app.config.ts", + "test-login-ci": "playwright test -c ./playwright-login.config.ts", + "test-ci": "playwright test -c ./playwright-app.config.ts", + "coverage-report-ci": "c8 report -c v8-reporter.config.json", "db:migrate": "node ./app/lib/fdm-migrate.server.js", "typecheck": "react-router typegen && tsc" }, @@ -82,6 +86,7 @@ }, "devDependencies": { "@dotenvx/dotenvx": "catalog:", + "@playwright/test": "^1.54.1", "@react-router/dev": "^7.9.1", "@react-router/fs-routes": "^7.9.1", "@svenvw/fdm-calculator": "workspace:*", @@ -97,6 +102,7 @@ "@types/react-dom": "^19.1.9", "@types/react-map-gl": "^6.1.7", "@types/validator": "^13.15.3", + "c8": "^10.1.3", "postcss": "^8.5.6", "tailwindcss": "^4.1.13", "typescript": "catalog:", diff --git a/fdm-app/playwright-app.config.ts b/fdm-app/playwright-app.config.ts new file mode 100644 index 000000000..2cfea65b6 --- /dev/null +++ b/fdm-app/playwright-app.config.ts @@ -0,0 +1,53 @@ +import { defineConfig, devices } from "@playwright/test" + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests/app", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI ? [["dot"], ["json"]] : [["list"], ["json"]], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:3000", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + ], + + /* Start the server in a CI environment such as GitHub Actions */ + webServer: { + command: + "pnpm dotenvx run -- c8 -c v8-reporter.config.json react-router-serve ./build/server/index.js", + url: "http://localhost:3000", + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI, + }, +}) diff --git a/fdm-app/playwright-login.config.ts b/fdm-app/playwright-login.config.ts new file mode 100644 index 000000000..1d9856373 --- /dev/null +++ b/fdm-app/playwright-login.config.ts @@ -0,0 +1,46 @@ +import { defineConfig, devices } from "@playwright/test" + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests/login", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI ? [["dot"], ["json"]] : [["list"], ["json"]], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:3000", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], + + /* Start the server in a CI environment such as GitHub Actions */ + webServer: { + command: + "pnpm dotenvx run -- c8 -c v8-reporter.config.json react-router-serve ./build/server/index.js", + env: { + WRITE_MAGIC_LINK_FILE: "1", + }, + url: "http://localhost:3000", + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI, + }, +}) diff --git a/fdm-app/tests/app/farm.$b_id_farm.fertilizers.test.ts b/fdm-app/tests/app/farm.$b_id_farm.fertilizers.test.ts new file mode 100644 index 000000000..b18fce774 --- /dev/null +++ b/fdm-app/tests/app/farm.$b_id_farm.fertilizers.test.ts @@ -0,0 +1,11 @@ +import { expect, test } from "@playwright/test" +import { loadSessionFromFile } from "../test-io" + +test.beforeEach(async ({ page, context }) => { + await loadSessionFromFile(context) + await page.goto("/farm") +}) + +test.fixme("There is a set of default fertilizers", async ({ page }) => { + await expect(page.getByRole("table")).toBeVisible() +}) diff --git a/fdm-app/tests/login/sign-in-and-farm.test.ts b/fdm-app/tests/login/sign-in-and-farm.test.ts new file mode 100644 index 000000000..3b6431aa6 --- /dev/null +++ b/fdm-app/tests/login/sign-in-and-farm.test.ts @@ -0,0 +1,113 @@ +import { test } from "../test-db" +import { + loadSessionFromFile, + saveSessionToFile, + testFileLine, + writeTestFile, +} from "../test-io" +import { submitCookieBannerWithTimeout } from "../util" +import { magicLinkUrlFileName } from "../test-io" + +const { expect } = test + +test.describe.configure({ mode: "serial" }) + +test("User can sign in via email and complete creating their profile", async ({ + page, + context, + sql, +}) => { + sql()`delete from "fdm-authn"."user" where email = 'xyz@example.com'` + + await writeTestFile(magicLinkUrlFileName, "") + + await page.goto("/") + await submitCookieBannerWithTimeout(page) + + const loginSubmitButton = page.getByRole("button", { name: "e-mail" }) + await expect( + loginSubmitButton, + "Login submit button does not exist.", + ).toBeVisible() + + const loginEmailBox = page.getByPlaceholder("e-mail") + await expect(loginEmailBox, "Email input box does not exist.").toBeVisible() + + await loginEmailBox.fill("xyz@example.com") + + await loginSubmitButton.click() + + await page.waitForURL("**/check-your-email") + const url = await testFileLine(magicLinkUrlFileName) + console.log(url) + await page.goto(url) + + const firstNameBox = page.getByLabel("Voornaam") + await expect(firstNameBox, "There is no first name box.").toBeVisible() + await firstNameBox.fill("Test") + const lastNameBox = page.getByLabel("Achternaam") + await expect(lastNameBox, "There is no last name box.").toBeVisible() + await lastNameBox.fill("User") + + const nameSubmitButton = page.getByRole("button", { name: "Doorgaan" }) + await expect(nameSubmitButton, "There is no submit button.").toBeVisible() + await nameSubmitButton.click() + + await page.waitForURL("/farm") + + await saveSessionToFile(context) +}) + +test.fixme( + "User can create a farm business and select parcels", + async ({ page, context }) => { + // Load session from previous test if it is missing for some reason + const currentCookies = await context.cookies() + if ( + !currentCookies.find( + (c) => c.name === "better-auth.session_token", + ) || + !currentCookies.find((c) => c.name === "toast-session") + ) { + await loadSessionFromFile(context) + } + + // Click the Create Business button + await page.goto("/farm") + const createBusinessButton = page.getByRole("link", { + name: "Start wizard", + }) + await expect( + createBusinessButton, + "There is no create business button", + ).toBeVisible() + await createBusinessButton.click() + + // Fill in the business creation form and submit + const businessNameBox = page.getByLabel("bedrijfsnaam") + await expect( + businessNameBox, + "There is no business name box", + ).toBeVisible() + await businessNameBox.fill("Example Business") + + const businessNameSubmitButton = page.getByRole("button", { + name: "volgende", + }) + await expect( + businessNameSubmitButton, + "There is no continue button after business name and address", + ).toBeVisible() + await businessNameSubmitButton.click() + + // Go to the Shape File Upload + const shapeFileUploadButton = page.getByRole("button", { + name: "Bestand uploaden", + }) + await expect( + shapeFileUploadButton, + "There is no shape file upload button", + ).toBeVisible() + await shapeFileUploadButton.click() + }, +) diff --git a/fdm-app/tests/test-db.ts b/fdm-app/tests/test-db.ts new file mode 100644 index 000000000..ed8289365 --- /dev/null +++ b/fdm-app/tests/test-db.ts @@ -0,0 +1,61 @@ +import base from "@playwright/test" +import postgres from "postgres" + +function makeDb() { + // Get credentials to connect to db + const host = + process.env.POSTGRES_HOST ?? + (() => { + throw new Error("POSTGRES_HOST environment variable is required") + })() + const port = + Number(process.env.POSTGRES_PORT) || + (() => { + throw new Error("POSTGRES_PORT environment variable is required") + })() + const user = + process.env.POSTGRES_USER ?? + (() => { + throw new Error("POSTGRES_USER environment variable is required") + })() + const password = + process.env.POSTGRES_PASSWORD ?? + (() => { + throw new Error( + "POSTGRES_PASSWORD environment variable is required", + ) + })() + const database = + process.env.POSTGRES_DB ?? + (() => { + throw new Error("POSTGRES_DB environment variable is required") + })() + + return postgres({ + host: host, + port: port, + database: database, + user: user, + password: password, + max: 1, + }) +} + +let sqlInstance + +export const test = base.extend< + {}, + { + sql: () => postgres.Sql + } +>({ + sql: [ + async ({}, use) => { + await use(() => { + sqlInstance ??= makeDb() + return sqlInstance + }) + }, + { scope: "worker" }, + ], +}) diff --git a/fdm-app/tests/test-io.ts b/fdm-app/tests/test-io.ts new file mode 100644 index 000000000..071586b10 --- /dev/null +++ b/fdm-app/tests/test-io.ts @@ -0,0 +1,109 @@ +import type { BrowserContext } from "@playwright/test" +import fs from "node:fs/promises" +import url from "node:url" + +export const testTmpDir = "../test-tmp/" +export const sessionFileName = "session.json" +export const magicLinkUrlFileName = "magicLink.txt" + +interface TestIOCommonOptions { + tmpUrl?: URL +} + +function testFileUrl(fileName: string, options: TestIOCommonOptions) { + return new URL( + fileName, + options.tmpUrl ?? new URL(testTmpDir, import.meta.url), + ) +} + +export function runtimeTestTmpUrl(cwd = process.cwd()) { + return new URL( + "./test-tmp/", + url.pathToFileURL( + cwd.endsWith("/") || cwd.endsWith("\\") ? cwd : `${cwd}/`, + ), + ) +} + +async function ensureSessionDir(options: TestIOCommonOptions) { + let exists = false + const url = options.tmpUrl ?? new URL(testTmpDir, import.meta.url) + try { + exists = (await fs.stat(url)).isDirectory() + } catch (_) {} + + if (!exists) { + await fs.mkdir(url, { recursive: true }) + } +} + +async function checkTestFileExists( + fileName: string, + options: TestIOCommonOptions, +) { + let exists = false + try { + exists = (await fs.stat(testFileUrl(fileName, options))).isFile() + } catch (_) {} + + return exists +} + +export function readTestFile( + fileName: string, + options: TestIOCommonOptions = {}, +) { + if (!checkTestFileExists(fileName, options)) + throw new Error(`Test file ${fileName} does not exist.`) + + return fs.readFile(testFileUrl(fileName, options), { encoding: "utf-8" }) +} + +export async function writeTestFile( + fileName: string, + contents: string, + flag = "w", + options: TestIOCommonOptions = {}, +) { + await ensureSessionDir(options) + return fs.writeFile(testFileUrl(fileName, options), contents, { + encoding: "utf-8", + flag, + }) +} + +export async function loadSessionFromFile(context: BrowserContext) { + const cookies = JSON.parse(await readTestFile(sessionFileName)) + + context.addCookies(cookies) +} + +export async function saveSessionToFile(context: BrowserContext) { + return writeTestFile( + sessionFileName, + JSON.stringify(await context.cookies()), + ) +} + +export async function writeTestFileLine( + fileName: string, + line: string, + options: TestIOCommonOptions = {}, +) { + return writeTestFile(fileName, `${line}\n`, "w+", options) +} + +export async function testFileLine( + fileName: string, + options: TestIOCommonOptions = {}, +) { + const readStream = await fs.open(testFileUrl(fileName, options)) + let myLine = "" + for await (const line of readStream.readLines()) { + myLine = line + break + } + readStream.close() + return myLine +} diff --git a/fdm-app/tests/util.ts b/fdm-app/tests/util.ts new file mode 100644 index 000000000..7f94cd03a --- /dev/null +++ b/fdm-app/tests/util.ts @@ -0,0 +1,30 @@ +import { expect } from "@playwright/test" + +export async function submitCookieBanner(page: Page) { + const cookieSubmitButton = page.getByRole("button", { name: "accept" }) + await expect(cookieSubmitButton).toBeVisible() + await cookieSubmitButton.click() +} + +export async function submitCookieBannerWithTimeout( + page: Page, + timeout = 1000, +) { + let cookieSubmitButton + await Promise.race([ + new Promise((resolve) => { + setTimeout(() => resolve(), timeout) + }), + (async () => { + const cand = page.getByRole("button", { + name: "accept", + }) + await expect(cand).toBeVisible() + cookieSubmitButton = cand + })(), + ]) + + if (cookieSubmitButton) { + await cookieSubmitButton.click() + } +} diff --git a/fdm-app/v8-reporter.config.json b/fdm-app/v8-reporter.config.json new file mode 100644 index 000000000..645e862db --- /dev/null +++ b/fdm-app/v8-reporter.config.json @@ -0,0 +1,9 @@ +{ + "reporter": ["text", "json", "html"], + "reports-dir": "./coverage", + "all": true, + "include": ["app/*", "build/*"], + "exclude": ["node_modules", "test/", "**/*.d.ts"], + "exclude-after-remap": true, + "extension": [".js", ".ts", ".tsx"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdeee45ae..3dd500ec8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -278,6 +278,9 @@ importers: '@dotenvx/dotenvx': specifier: 'catalog:' version: 1.48.4 + '@playwright/test': + specifier: ^1.54.1 + version: 1.56.0 '@react-router/dev': specifier: ^7.9.1 version: 7.9.1(@react-router/serve@7.9.1(react-router@7.9.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.9.2))(@types/node@24.5.2)(@vitejs/plugin-rsc@0.4.11(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(vite@7.1.6(@types/node@24.5.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1)))(jiti@2.5.1)(lightningcss@1.30.1)(react-router@7.9.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(terser@5.44.0)(typescript@5.9.2)(vite@7.1.6(@types/node@24.5.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1) @@ -314,6 +317,9 @@ importers: '@types/validator': specifier: ^13.15.3 version: 13.15.3 + c8: + specifier: ^10.1.3 + version: 10.1.3 postcss: specifier: ^8.5.6 version: 8.5.6 @@ -2611,6 +2617,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.56.0': + resolution: {integrity: sha512-Tzh95Twig7hUwwNe381/K3PggZBZblKUe2wv25oIpzWLr6Z0m4KgV1ZVIjnR6GM9ANEqjZD7XsZEa6JL/7YEgg==} + engines: {node: '>=18'} + hasBin: true + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -5348,6 +5359,16 @@ packages: bytewise@1.1.0: resolution: {integrity: sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==} + c8@10.1.3: + resolution: {integrity: sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + monocart-coverage-reports: ^2 + peerDependenciesMeta: + monocart-coverage-reports: + optional: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -5499,6 +5520,10 @@ packages: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clone-deep@4.0.1: resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} engines: {node: '>=6'} @@ -6566,6 +6591,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -6608,6 +6638,10 @@ packages: resolution: {integrity: sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==} engines: {node: '>=10.19'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -8327,6 +8361,16 @@ packages: resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} engines: {node: '>=14.16'} + playwright-core@1.56.0: + resolution: {integrity: sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.56.0: + resolution: {integrity: sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA==} + engines: {node: '>=18'} + hasBin: true + point-in-polygon-hao@1.2.4: resolution: {integrity: sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ==} @@ -9277,6 +9321,10 @@ packages: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} engines: {node: '>=0.10'} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -10227,6 +10275,10 @@ packages: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + valibot@0.41.0: resolution: {integrity: sha512-igDBb8CTYr8YTQlOKgaN9nSS0Be7z+WRuaeYqGf3Cjz3aKmSnqEmYnkfVjzIuumGqfHpa3fLIvMEAfhrpqN8ng==} peerDependencies: @@ -10541,6 +10593,10 @@ packages: xxhash-wasm@1.1.0: resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -10560,6 +10616,14 @@ packages: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -13607,6 +13671,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.56.0': + dependencies: + playwright: 1.56.0 + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -17317,6 +17385,20 @@ snapshots: bytewise-core: 1.2.3 typewise: 1.0.3 + c8@10.1.3: + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@istanbuljs/schema': 0.1.3 + find-up: 5.0.0 + foreground-child: 3.3.1 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + test-exclude: 7.0.1 + v8-to-istanbul: 9.3.0 + yargs: 17.7.2 + yargs-parser: 21.1.1 + cac@6.7.14: {} cacheable-lookup@5.0.4: {} @@ -17482,6 +17564,12 @@ snapshots: optionalDependencies: '@colors/colors': 1.5.0 + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clone-deep@4.0.1: dependencies: is-plain-object: 2.0.4 @@ -18571,6 +18659,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -18616,6 +18707,8 @@ snapshots: xml-utils: 1.10.2 zstddec: 0.1.0 + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -20670,6 +20763,14 @@ snapshots: dependencies: find-up: 6.3.0 + playwright-core@1.56.0: {} + + playwright@1.56.0: + dependencies: + playwright-core: 1.56.0 + optionalDependencies: + fsevents: 2.3.2 + point-in-polygon-hao@1.2.4: dependencies: robust-predicates: 3.0.2 @@ -21765,6 +21866,8 @@ snapshots: repeat-string@1.6.1: {} + require-directory@2.1.1: {} + require-from-string@2.0.2: {} require-in-the-middle@7.5.2: @@ -22769,6 +22872,12 @@ snapshots: uuid@8.3.2: {} + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + valibot@0.41.0(typescript@5.9.2): optionalDependencies: typescript: 5.9.2 @@ -23173,6 +23282,8 @@ snapshots: xxhash-wasm@1.1.0: {} + y18n@5.0.8: {} + yallist@3.1.1: {} yallist@4.0.0: {} @@ -23183,6 +23294,18 @@ snapshots: yargs-parser@20.2.9: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} yocto-queue@1.2.1: {}