diff --git a/apps/dev-playground/.gitignore b/apps/dev-playground/.gitignore new file mode 100644 index 00000000..5c957fcd --- /dev/null +++ b/apps/dev-playground/.gitignore @@ -0,0 +1,3 @@ +# Playwright +test-results/ +playwright-report/ \ No newline at end of file diff --git a/apps/dev-playground/README.md b/apps/dev-playground/README.md new file mode 100644 index 00000000..cf06ab12 --- /dev/null +++ b/apps/dev-playground/README.md @@ -0,0 +1,41 @@ +# Dev Playground + +Test application showing AppKit capabilities including analytics dashboards, SSE streaming, telemetry, and data visualization. + +## Development + +```bash +# Start development server +pnpm dev + +# Build for production +pnpm build + +# Start production server +pnpm start:local +``` + +## Integration Tests + +Integration tests use Playwright to verify the application works correctly with mocked backend responses. + +**Note:** These are frontend-only integration tests. API calls are intercepted at the browser level and return mock data, so the AppKit backend plugins are not tested. They focus on verifying UI behavior, navigation, data rendering, and client-side interactions. + +### Running Tests + +```bash +# Run all integration tests +pnpm test:integration + +# Run tests with interactive UI mode (for debugging) +pnpm test:integration:ui + +# Run tests in headed mode (see the browser) +pnpm test:integration:headed + +# Run a specific test file +npx playwright test tests/smoke.spec.ts + +# Run tests matching a pattern +npx playwright test -g "analytics" +``` diff --git a/apps/dev-playground/client/package-lock.json b/apps/dev-playground/client/package-lock.json index a0cd6cd5..22b20739 100644 --- a/apps/dev-playground/client/package-lock.json +++ b/apps/dev-playground/client/package-lock.json @@ -113,6 +113,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -2615,6 +2616,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.134.9.tgz", "integrity": "sha512-JIxFamShs3gRIkOxpgz/3bglbSKZHMrzKASwNFg+sQPVXVPOLtN35D5PuEDAFTPPht9Wv48WWUNYE03ZytnNug==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.133.28", "@tanstack/react-store": "^0.8.0", @@ -2722,6 +2724,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.134.9.tgz", "integrity": "sha512-9Vr8tYC59I70DYGVRknRf4vjQMjSfHvmc+iTM8vcpwERBh3Vgkv90f8ol85KHKqjorSsCqMeYFhFt8AM4A4CSw==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.133.28", "@tanstack/store": "^0.8.0", @@ -3061,6 +3064,7 @@ "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.14.0" } @@ -3071,6 +3075,7 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3081,6 +3086,7 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3144,6 +3150,7 @@ "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", @@ -3402,6 +3409,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3625,6 +3633,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -3923,7 +3932,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/d3-array": { "version": "3.2.4", @@ -4223,6 +4233,7 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5590,6 +5601,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5678,6 +5690,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5687,6 +5700,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -5706,6 +5720,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -5874,7 +5889,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -6041,6 +6057,7 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -6193,7 +6210,8 @@ "version": "4.1.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -6222,7 +6240,8 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tiny-warning": { "version": "1.0.3", @@ -6268,6 +6287,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6364,6 +6384,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6653,6 +6674,7 @@ "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.1.14.tgz", "integrity": "sha512-eSiiRJmovt8qDJkGyZuLnbxAOAdie6NCmmd0NkTC0RJI9duiSBTfr8X2mBYJOUFzxQa2USaHmL99J9uMxkjCyw==", "license": "MIT", + "peer": true, "dependencies": { "@oxc-project/runtime": "0.92.0", "fdir": "^6.5.0", @@ -6745,6 +6767,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/apps/dev-playground/client/src/routes/telemetry.route.tsx b/apps/dev-playground/client/src/routes/telemetry.route.tsx index 5291da67..da07aadf 100644 --- a/apps/dev-playground/client/src/routes/telemetry.route.tsx +++ b/apps/dev-playground/client/src/routes/telemetry.route.tsx @@ -106,9 +106,7 @@ function TelemetryRoute() {
Demonstrates how the SDK's auto-instrumentation integrates with custom application telemetry. This example showcases HTTP and cache diff --git a/apps/dev-playground/package.json b/apps/dev-playground/package.json index 98b1f515..b5c3c0b8 100644 --- a/apps/dev-playground/package.json +++ b/apps/dev-playground/package.json @@ -14,7 +14,10 @@ "preview": "vite preview", "check": "tsc", "clean": "rm -rf build && cd client && rm -rf dist", - "clean:full": "rm -rf build node_modules && cd client && rm -rf dist node_modules" + "clean:full": "rm -rf build node_modules && cd client && rm -rf dist node_modules", + "test:integration": "playwright test", + "test:integration:ui": "playwright test --ui", + "test:integration:headed": "playwright test --headed" }, "keywords": [], "author": "", @@ -25,6 +28,7 @@ "zod": "^4.1.13" }, "devDependencies": { + "@playwright/test": "^1.50.1", "@types/node": "^20.0.0", "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^5.0.4", diff --git a/apps/dev-playground/playwright.config.ts b/apps/dev-playground/playwright.config.ts new file mode 100644 index 00000000..b0ca6f92 --- /dev/null +++ b/apps/dev-playground/playwright.config.ts @@ -0,0 +1,26 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: 0, + workers: process.env.CI ? 1 : undefined, + reporter: "html", + use: { + baseURL: "http://localhost:8000", + trace: "on-first-retry", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], + webServer: { + command: "pnpm dev", + url: "http://localhost:8000", + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, +}); diff --git a/apps/dev-playground/tests/arrow-analytics.spec.ts b/apps/dev-playground/tests/arrow-analytics.spec.ts new file mode 100644 index 00000000..497d29bd --- /dev/null +++ b/apps/dev-playground/tests/arrow-analytics.spec.ts @@ -0,0 +1,101 @@ +import { expect, test } from "@playwright/test"; +import { + STRICT_MODE_MULTIPLIER, + setupMockAPI, + trackApiCalls, + waitForChartsToLoad, + waitForPageLoad, +} from "./fixtures/test-utils"; + +test.describe("Arrow Analytics", () => { + test.beforeEach(async ({ page }) => { + await setupMockAPI(page); + }); + + test("page loads and displays heading", async ({ page }) => { + await page.goto("/arrow-analytics"); + await waitForPageLoad(page); + await expect(page.getByText("Unified Charts API")).toBeVisible(); + }); + + test("calls expected API endpoints", async ({ page }) => { + const appsListCalls = trackApiCalls(page, "apps_list"); + const spendDataCalls = trackApiCalls(page, "spend_data"); + const topContributorsCalls = trackApiCalls(page, "top_contributors"); + const heatmapCalls = trackApiCalls(page, "app_activity_heatmap"); + + await page.goto("/arrow-analytics"); + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await waitForChartsToLoad(page); + + expect(appsListCalls.length).toBe(5 * STRICT_MODE_MULTIPLIER); + expect(spendDataCalls.length).toBe(5 * STRICT_MODE_MULTIPLIER); + expect(topContributorsCalls.length).toBe(2 * STRICT_MODE_MULTIPLIER); + expect(heatmapCalls.length).toBe(2 * STRICT_MODE_MULTIPLIER); + }); + + test("charts render with mock data (no empty states)", async ({ page }) => { + await page.goto("/arrow-analytics"); + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await waitForChartsToLoad(page); + + const barCharts = page.locator('[data-testid="bar-chart-apps_list"]'); + expect(await barCharts.count()).toBe(3); + await expect(barCharts.first().locator("canvas")).toBeVisible(); + + const lineCharts = page.locator('[data-testid="line-chart-spend_data"]'); + expect(await lineCharts.count()).toBe(3); + await expect(lineCharts.first().locator("canvas")).toBeVisible(); + + const donutCharts = page.locator( + '[data-testid="donut-chart-top_contributors"]', + ); + expect(await donutCharts.count()).toBe(2); + await expect(donutCharts.first().locator("canvas")).toBeVisible(); + + const heatmapCharts = page.locator( + '[data-testid="heatmap-chart-app_activity_heatmap"]', + ); + expect(await heatmapCharts.count()).toBe(2); + await expect(heatmapCharts.first().locator("canvas")).toBeVisible(); + }); + + test("chart tooltip appears on hover with mock app data", async ({ + page, + }) => { + await page.goto("/arrow-analytics"); + await waitForChartsToLoad(page); + + const barChart = page + .locator('[data-testid="bar-chart-apps_list"]') + .first() + .locator("canvas"); + await expect(barChart).toBeVisible(); + + const box = await barChart.boundingBox(); + if (!box) throw new Error("Could not get chart bounding box"); + + // Try multiple positions to find a data point (bar positions vary by chart size) + const positions = [0.2, 0.35, 0.5, 0.65, 0.8]; + for (const xRatio of positions) { + await page.mouse.move( + box.x + box.width * xRatio, + box.y + box.height * 0.4, + ); + + // Check if tooltip appeared with any of our mock app names + const tooltip = page + .locator("div") + .filter({ hasText: /App One|App Two|App Three/ }); + + try { + await expect(tooltip.first()).toBeVisible({ timeout: 1000 }); + return; // Test passed - tooltip found + } catch {} + } + + throw new Error( + "Could not trigger tooltip with any mock app data after trying multiple positions", + ); + }); +}); diff --git a/apps/dev-playground/tests/data-visualization.spec.ts b/apps/dev-playground/tests/data-visualization.spec.ts new file mode 100644 index 00000000..07749bda --- /dev/null +++ b/apps/dev-playground/tests/data-visualization.spec.ts @@ -0,0 +1,100 @@ +import { expect, test } from "@playwright/test"; +import { + STRICT_MODE_MULTIPLIER, + setupMockAPI, + trackApiCalls, + waitForPageLoad, +} from "./fixtures/test-utils"; + +test.describe("Data Visualization Route Tests", () => { + test.beforeEach(async ({ page }) => { + await setupMockAPI(page); + }); + + test("data-visualization page loads successfully", async ({ page }) => { + await page.goto("/data-visualization"); + await waitForPageLoad(page); + + await expect(page).toHaveURL("/data-visualization"); + }); + + test("page displays Data Visualization heading", async ({ page }) => { + await page.goto("/data-visualization"); + await waitForPageLoad(page); + + await expect(page.getByText("Data Visualization")).toBeVisible(); + }); + + test("simple data table displays mock data", async ({ page }) => { + await page.goto("/data-visualization"); + await waitForPageLoad(page); + + // The simple table is the first table on the page + const simpleTable = page.locator("table").nth(0); + + await simpleTable.scrollIntoViewIfNeeded(); + await expect(simpleTable).toBeVisible(); + + // Verify the table contains expected mock data cells + await expect( + simpleTable.getByRole("cell", { name: "Untagged App 1" }), + ).toBeVisible(); + await expect( + simpleTable.getByRole("cell", { name: "user4@databricks.com" }), + ).toBeVisible(); + }); + + test("advanced data table displays mock data", async ({ page }) => { + await page.goto("/data-visualization"); + await waitForPageLoad(page); + + // The advanced table is the second table on the page + const advancedTable = page.locator("table").nth(1); + + await advancedTable.scrollIntoViewIfNeeded(); + await expect(advancedTable).toBeVisible(); + + // Verify the table contains expected mock data cells + await expect( + advancedTable.getByRole("cell", { name: "Untagged App 2" }), + ).toBeVisible(); + await expect( + advancedTable.getByRole("cell", { name: "user5@databricks.com" }), + ).toBeVisible(); + }); + + test("calls expected API endpoints on page load", async ({ page }) => { + const untaggedAppsCalls = trackApiCalls(page, "untagged_apps"); + const spendDataCalls = trackApiCalls(page, "spend_data"); + const topContributorsCalls = trackApiCalls(page, "top_contributors"); + + await page.goto("/data-visualization"); + await waitForPageLoad(page); + + // Scroll to load all charts and tables + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await page.waitForLoadState("networkidle"); + + // Verify API calls: 2 tables use untagged_apps + expect(untaggedAppsCalls.length).toBe(2 * STRICT_MODE_MULTIPLIER); + // Multiple charts use spend_data (AreaChart x2, LineChart x2, RadarChart x2) + expect(spendDataCalls.length).toBe(6 * STRICT_MODE_MULTIPLIER); + // BarChart x2, PieChart x2 use top_contributors + expect(topContributorsCalls.length).toBe(4 * STRICT_MODE_MULTIPLIER); + }); + + test("can toggle code visibility", async ({ page }) => { + await page.goto("/data-visualization"); + await waitForPageLoad(page); + + const showCodeButton = page + .getByRole("button", { name: "Show Code" }) + .first(); + await showCodeButton.click(); + + // Verify code section is revealed by checking the Hide Code button appears + await expect( + page.getByRole("button", { name: "Hide Code" }).first(), + ).toBeVisible(); + }); +}); diff --git a/apps/dev-playground/tests/fixtures/mock-data.ts b/apps/dev-playground/tests/fixtures/mock-data.ts new file mode 100644 index 00000000..39a6adc0 --- /dev/null +++ b/apps/dev-playground/tests/fixtures/mock-data.ts @@ -0,0 +1,180 @@ +/** + * Mock data for dev-playground frontend integration tests + * These mock responses simulate the API responses from the backend + */ + +// Analytics mock data +export const mockAnalyticsData = { + spendSummary: [ + { + total: 15000, + average: 500, + forecasted: 18000, + }, + ], + + appsList: [ + { + id: 1, + name: "App One", + creator: "user1@databricks.com", + totalSpend: 5000, + tags: ["prod", "analytics"], + createdAt: "2024-01-15T10:30:00Z", + }, + { + id: 2, + name: "App Two", + creator: "user2@databricks.com", + totalSpend: 3500, + tags: ["staging"], + createdAt: "2024-02-20T14:45:00Z", + }, + { + id: 3, + name: "App Three", + creator: "user3@databricks.com", + totalSpend: 2200, + tags: [], + createdAt: "2024-03-10T09:00:00Z", + }, + ], + + untaggedApps: [ + { + app_name: "Untagged App 1", + creator: "user4@databricks.com", + total_cost_usd: 1200, + avg_period_cost_usd: 400, + }, + { + app_name: "Untagged App 2", + creator: "user5@databricks.com", + total_cost_usd: 800, + avg_period_cost_usd: 266, + }, + ], + + spendData: [ + { + aggregation_period: "2024-01-01", + cost_usd: 1200, + group_key: "default", + }, + { + aggregation_period: "2024-01-02", + cost_usd: 1350, + group_key: "default", + }, + { + aggregation_period: "2024-01-03", + cost_usd: 980, + group_key: "default", + }, + { + aggregation_period: "2024-01-04", + cost_usd: 1500, + group_key: "default", + }, + { + aggregation_period: "2024-01-05", + cost_usd: 1100, + group_key: "default", + }, + ], + + topContributors: [ + { app_name: "Top App 1", total_cost_usd: 5000 }, + { app_name: "Top App 2", total_cost_usd: 3500 }, + { app_name: "Top App 3", total_cost_usd: 2800 }, + { app_name: "Top App 4", total_cost_usd: 2200 }, + { app_name: "Top App 5", total_cost_usd: 1500 }, + ], + + appActivityHeatmap: [ + { app_name: "App One", day_of_week: "Monday", spend: 500 }, + { app_name: "App One", day_of_week: "Tuesday", spend: 600 }, + { app_name: "App One", day_of_week: "Wednesday", spend: 450 }, + { app_name: "App Two", day_of_week: "Monday", spend: 300 }, + { app_name: "App Two", day_of_week: "Tuesday", spend: 400 }, + { app_name: "App Two", day_of_week: "Wednesday", spend: 350 }, + ], + + sqlHelpersTest: [ + { + string_value: "Hello, Databricks!", + number_value: 42, + boolean_value: true, + date_value: "2024-01-15", + timestamp_value: "2024-01-15T10:30:00Z", + binary_value: "Spark", + binary_hex: "537061726B", + binary_length: 5, + }, + ], +}; + +// Reconnect/SSE mock data +export const mockReconnectMessages = [ + { + type: "message", + count: 1, + total: 5, + timestamp: new Date().toISOString(), + content: "Message 1 of 5", + }, + { + type: "message", + count: 2, + total: 5, + timestamp: new Date().toISOString(), + content: "Message 2 of 5", + }, + { + type: "message", + count: 3, + total: 5, + timestamp: new Date().toISOString(), + content: "Message 3 of 5", + }, + { + type: "message", + count: 4, + total: 5, + timestamp: new Date().toISOString(), + content: "Message 4 of 5", + }, + { + type: "message", + count: 5, + total: 5, + timestamp: new Date().toISOString(), + content: "Message 5 of 5", + }, +]; + +// Telemetry mock data +export const mockTelemetryResponse = { + success: true, + message: "Telemetry example completed successfully", + duration_ms: 150, + result: { items_processed: 5 }, + tracing: { + hint: "View traces in Grafana", + services: ["telemetry-examples"], + expectedSpans: [ + "telemetry-examples.combined", + "cache-lookup", + "http-request", + ], + }, + metrics: { + recorded: [ + "telemetry_examples.requests_total", + "telemetry_examples.request_duration_ms", + ], + }, + logs: { + emitted: ["Processing started", "Cache miss", "Processing completed"], + }, +}; diff --git a/apps/dev-playground/tests/fixtures/test-utils.ts b/apps/dev-playground/tests/fixtures/test-utils.ts new file mode 100644 index 00000000..dbe6d280 --- /dev/null +++ b/apps/dev-playground/tests/fixtures/test-utils.ts @@ -0,0 +1,140 @@ +import type { Page, Request } from "@playwright/test"; +import { + mockAnalyticsData, + mockReconnectMessages, + mockTelemetryResponse, +} from "./mock-data"; + +/** + * React 19 Strict Mode doubles useEffect invocations in development mode + * to help detect side effects. This multiplier accounts for that behavior + * when asserting API call counts in tests. + * + * @see https://react.dev/reference/react/StrictMode#fixing-bugs-found-by-re-running-effects-in-development + */ +export const STRICT_MODE_MULTIPLIER = 2; + +function createSSEResponse(data: unknown): string { + const event = JSON.stringify({ type: "result", data }); + return `data: ${event}\n\n`; +} + +function getSSEHeaders() { + return { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }; +} + +export async function setupMockAPI(page: Page) { + await page.route("**/api/analytics/query/**", async (route) => { + const url = route.request().url(); + + if (url.includes("spend_summary")) { + return route.fulfill({ + status: 200, + headers: getSSEHeaders(), + body: createSSEResponse(mockAnalyticsData.spendSummary), + }); + } + if (url.includes("apps_list")) { + return route.fulfill({ + status: 200, + headers: getSSEHeaders(), + body: createSSEResponse(mockAnalyticsData.appsList), + }); + } + if (url.includes("untagged_apps")) { + return route.fulfill({ + status: 200, + headers: getSSEHeaders(), + body: createSSEResponse(mockAnalyticsData.untaggedApps), + }); + } + if (url.includes("spend_data")) { + return route.fulfill({ + status: 200, + headers: getSSEHeaders(), + body: createSSEResponse(mockAnalyticsData.spendData), + }); + } + if (url.includes("top_contributors")) { + return route.fulfill({ + status: 200, + headers: getSSEHeaders(), + body: createSSEResponse(mockAnalyticsData.topContributors), + }); + } + if (url.includes("app_activity_heatmap")) { + return route.fulfill({ + status: 200, + headers: getSSEHeaders(), + body: createSSEResponse(mockAnalyticsData.appActivityHeatmap), + }); + } + if (url.includes("sql_helpers_test")) { + return route.fulfill({ + status: 200, + headers: getSSEHeaders(), + body: createSSEResponse(mockAnalyticsData.sqlHelpersTest), + }); + } + + // Default empty response for unknown queries + return route.fulfill({ + status: 200, + headers: getSSEHeaders(), + body: createSSEResponse([]), + }); + }); + + await page.route("**/api/reconnect/stream**", async (route) => { + const body = mockReconnectMessages + .map((msg, i) => `id: ${i + 1}\ndata: ${JSON.stringify(msg)}\n\n`) + .join(""); + + return route.fulfill({ + status: 200, + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + body, + }); + }); + + await page.route("**/api/reconnect", async (route) => { + if (route.request().url().endsWith("/api/reconnect")) { + return route.fulfill({ json: { message: "Reconnected" } }); + } + return route.continue(); + }); + + await page.route("**/api/telemetry-examples/**", async (route) => { + return route.fulfill({ json: mockTelemetryResponse }); + }); +} + +export async function waitForPageLoad(page: Page) { + await page.waitForLoadState("networkidle"); +} + +export async function waitForChartsToLoad(page: Page) { + await waitForPageLoad(page); + await page.waitForFunction( + () => document.querySelectorAll(".animate-pulse").length === 0, + { timeout: 10000 }, + ); +} + +export function trackApiCalls(page: Page, urlPattern: string) { + const requests: Request[] = []; + page.on("request", (request) => { + if (request.url().includes(urlPattern)) { + requests.push(request); + } + }); + return requests; +} diff --git a/apps/dev-playground/tests/navigation.spec.ts b/apps/dev-playground/tests/navigation.spec.ts new file mode 100644 index 00000000..e761488a --- /dev/null +++ b/apps/dev-playground/tests/navigation.spec.ts @@ -0,0 +1,128 @@ +import { expect, test } from "@playwright/test"; +import { setupMockAPI, waitForPageLoad } from "./fixtures/test-utils"; + +const homepageNavigationTests: Array<{ + name: string; + buttonName: string; + buttonIndex?: number; + expectedUrl: string; +}> = [ + { + name: "analytics", + buttonName: "Explore real-time analytics", + buttonIndex: 0, + expectedUrl: "/analytics", + }, + { + name: "arrow-analytics", + buttonName: "Explore real-time analytics", + buttonIndex: 1, + expectedUrl: "/arrow-analytics", + }, + { + name: "reconnect", + buttonName: "View Reconnect Demo", + expectedUrl: "/reconnect", + }, + { + name: "data-visualization", + buttonName: "Explore data visualization", + expectedUrl: "/data-visualization", + }, + { + name: "telemetry", + buttonName: "Try Telemetry Examples", + expectedUrl: "/telemetry", + }, + { + name: "sql-helpers", + buttonName: "Try SQL Helpers", + expectedUrl: "/sql-helpers", + }, + { + name: "type-safety", + buttonName: "Explore Type Safety", + expectedUrl: "/type-safety", + }, +]; + +test.describe("Navigation Tests", () => { + test.beforeEach(async ({ page }) => { + await setupMockAPI(page); + }); + + for (const { + name, + buttonName, + buttonIndex, + expectedUrl, + } of homepageNavigationTests) { + test(`can navigate to ${name} from homepage`, async ({ page }) => { + await page.goto("/"); + await waitForPageLoad(page); + + const button = page.getByRole("button", { name: buttonName }); + if (buttonIndex !== undefined) { + await button.nth(buttonIndex).click(); + } else { + await button.click(); + } + + await expect(page).toHaveURL(expectedUrl); + }); + } + + test("navigation bar shows on non-home pages", async ({ page }) => { + await page.goto("/analytics"); + await waitForPageLoad(page); + + await expect( + page.getByRole("link", { name: "AppKit Playground" }), + ).toBeVisible(); + + await expect( + page.getByRole("button", { name: "Analytics", exact: true }), + ).toBeVisible(); + await expect( + page.getByRole("button", { name: "Arrow Analytics" }), + ).toBeVisible(); + await expect(page.getByRole("button", { name: "Reconnect" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Telemetry" })).toBeVisible(); + await expect( + page.getByRole("button", { name: "SQL Helpers" }), + ).toBeVisible(); + }); + + test("can navigate back to home from nav bar", async ({ page }) => { + await page.goto("/analytics"); + await waitForPageLoad(page); + + await page.getByRole("link", { name: "AppKit Playground" }).click(); + + await expect(page).toHaveURL("/"); + }); + + test("can navigate between pages using nav bar", async ({ page }) => { + await page.goto("/analytics"); + await waitForPageLoad(page); + + await page.getByRole("button", { name: "Reconnect" }).click(); + await expect(page).toHaveURL("/reconnect"); + + await page.getByRole("button", { name: "Telemetry" }).click(); + await expect(page).toHaveURL("/telemetry"); + + await page.getByRole("button", { name: "SQL Helpers" }).click(); + await expect(page).toHaveURL("/sql-helpers"); + }); + + test("navigation bar is hidden on homepage", async ({ page }) => { + await page.goto("/"); + await waitForPageLoad(page); + + const navBar = page.locator("nav").filter({ + has: page.getByRole("button", { name: "Analytics" }), + }); + await expect(navBar).not.toBeVisible(); + }); +}); diff --git a/apps/dev-playground/tests/reconnect.spec.ts b/apps/dev-playground/tests/reconnect.spec.ts new file mode 100644 index 00000000..6d3a9a82 --- /dev/null +++ b/apps/dev-playground/tests/reconnect.spec.ts @@ -0,0 +1,50 @@ +import { expect, test } from "@playwright/test"; +import { setupMockAPI, waitForPageLoad } from "./fixtures/test-utils"; + +test.describe("Reconnect Route Tests", () => { + test.beforeEach(async ({ page }) => { + await setupMockAPI(page); + }); + + test("reconnect page loads successfully", async ({ page }) => { + await page.goto("/reconnect"); + await waitForPageLoad(page); + + await expect(page).toHaveURL("/reconnect"); + }); + + test("connects to SSE stream and receives all messages", async ({ page }) => { + const streamRequestPromise = page.waitForRequest((request) => + request.url().includes("/api/reconnect/stream"), + ); + + await page.goto("/reconnect"); + await streamRequestPromise; + await waitForPageLoad(page); + + await expect( + page.locator('[data-slot="badge"]').filter({ hasText: "Reconnected" }), + ).toBeVisible({ timeout: 5000 }); + + const messageCountContainer = page.locator("div").filter({ + has: page.getByText("/ 5 messages received"), + }); + await expect(messageCountContainer.locator("h2")).toHaveText("5", { + timeout: 5000, + }); + }); + + test("restart button triggers new stream connection", async ({ page }) => { + await page.goto("/reconnect"); + await waitForPageLoad(page); + + const newStreamRequestPromise = page.waitForRequest((request) => + request.url().includes("/api/reconnect/stream"), + ); + + const restartButton = page.getByRole("button", { name: /restart/i }); + await restartButton.click(); + + await newStreamRequestPromise; + }); +}); diff --git a/apps/dev-playground/tests/smoke.spec.ts b/apps/dev-playground/tests/smoke.spec.ts new file mode 100644 index 00000000..dcd9aa66 --- /dev/null +++ b/apps/dev-playground/tests/smoke.spec.ts @@ -0,0 +1,78 @@ +import { expect, test } from "@playwright/test"; +import { setupMockAPI, waitForPageLoad } from "./fixtures/test-utils"; + +test.describe("Smoke Tests", () => { + test.beforeEach(async ({ page }) => { + await setupMockAPI(page); + }); + + test("app loads and displays homepage", async ({ page }) => { + await page.goto("/"); + await waitForPageLoad(page); + + await expect( + page.getByRole("heading", { name: "AppKit Playground" }), + ).toBeVisible(); + + await expect( + page.getByText("Explore the capabilities of the AppKit"), + ).toBeVisible(); + }); + + test("all feature cards are visible on homepage", async ({ page }) => { + await page.goto("/"); + await waitForPageLoad(page); + + await expect( + page.getByText("Analytics Dashboard", { exact: true }), + ).toBeVisible(); + await expect( + page.getByText("Arrow Analytics Dashboard", { exact: true }), + ).toBeVisible(); + await expect( + page.getByText("Stream Reconnection", { exact: true }), + ).toBeVisible(); + await expect( + page.getByText("Data Visualization", { exact: true }), + ).toBeVisible(); + await expect(page.getByText("Telemetry", { exact: true })).toBeVisible(); + await expect(page.getByText("SQL Helpers", { exact: true })).toBeVisible(); + await expect( + page.getByText("Type-Safe SQL", { exact: true }), + ).toBeVisible(); + }); + + test("theme selector is visible on homepage", async ({ page }) => { + await page.goto("/"); + await waitForPageLoad(page); + + const themeButton = page.locator('[class*="top-4"][class*="right-4"]'); + await expect(themeButton).toBeVisible(); + }); + + test("no console errors on page load", async ({ page }) => { + const consoleErrors: string[] = []; + + page.on("console", (msg) => { + if (msg.type() === "error") { + consoleErrors.push(msg.text()); + } + }); + + await page.goto("/"); + await waitForPageLoad(page); + + expect( + consoleErrors, + `Console errors detected: ${consoleErrors.join(", ")}`, + ).toHaveLength(0); + }); + + test("page has a title", async ({ page }) => { + await page.goto("/"); + await waitForPageLoad(page); + + const title = await page.title(); + expect(title).toBeTruthy(); + }); +}); diff --git a/apps/dev-playground/tests/sql-helpers.spec.ts b/apps/dev-playground/tests/sql-helpers.spec.ts new file mode 100644 index 00000000..5e31b8cd --- /dev/null +++ b/apps/dev-playground/tests/sql-helpers.spec.ts @@ -0,0 +1,76 @@ +import { expect, test } from "@playwright/test"; +import { mockAnalyticsData } from "./fixtures/mock-data"; +import { setupMockAPI, waitForPageLoad } from "./fixtures/test-utils"; + +test.describe("SQL Helpers Route Tests", () => { + test.beforeEach(async ({ page }) => { + await setupMockAPI(page); + }); + + test("sql-helpers page loads successfully", async ({ page }) => { + await page.goto("/sql-helpers"); + await waitForPageLoad(page); + + await expect(page).toHaveURL("/sql-helpers"); + }); + + test("can interact with string input", async ({ page }) => { + await page.goto("/sql-helpers"); + await waitForPageLoad(page); + + const inputs = page.locator("input").first(); + await inputs.fill("Test String Value"); + + await expect(inputs).toHaveValue("Test String Value"); + }); + + test("can interact with number input", async ({ page }) => { + await page.goto("/sql-helpers"); + await waitForPageLoad(page); + + const numberInput = page.locator('input[type="number"]').first(); + await numberInput.fill("123"); + + await expect(numberInput).toHaveValue("123"); + }); + + test("can toggle boolean value", async ({ page }) => { + await page.goto("/sql-helpers"); + await waitForPageLoad(page); + + const falseButton = page.getByRole("button", { name: "false" }); + await falseButton.click(); + + const trueButton = page.getByRole("button", { name: "true" }); + await expect(trueButton).toBeVisible(); + }); + + test("show code button reveals code example", async ({ page }) => { + await page.goto("/sql-helpers"); + await waitForPageLoad(page); + + const showCodeButton = page + .getByRole("button", { name: "Show Code" }) + .first(); + await showCodeButton.click(); + + await expect(page.getByText("Usage:").first()).toBeVisible(); + }); + + test("sql_helpers_test query executes and displays mock data", async ({ + page, + }) => { + await page.goto("/sql-helpers"); + await waitForPageLoad(page); + + await expect(page.getByText("Query executed successfully")).toBeVisible({ + timeout: 5000, + }); + + const resultPre = page.locator(".bg-success\\/10 pre"); + await expect(resultPre).toBeVisible(); + await expect(resultPre).toHaveText( + JSON.stringify(mockAnalyticsData.sqlHelpersTest[0], null, 2), + ); + }); +}); diff --git a/apps/dev-playground/tests/telemetry.spec.ts b/apps/dev-playground/tests/telemetry.spec.ts new file mode 100644 index 00000000..d81b26ea --- /dev/null +++ b/apps/dev-playground/tests/telemetry.spec.ts @@ -0,0 +1,36 @@ +import { expect, test } from "@playwright/test"; +import { + setupMockAPI, + trackApiCalls, + waitForPageLoad, +} from "./fixtures/test-utils"; + +test.describe("Telemetry Route Tests", () => { + test.beforeEach(async ({ page }) => { + await setupMockAPI(page); + }); + + test("telemetry page loads successfully", async ({ page }) => { + await page.goto("/telemetry"); + await waitForPageLoad(page); + + await expect(page).toHaveURL("/telemetry"); + }); + + test("run button triggers POST request and shows success", async ({ + page, + }) => { + const requests = trackApiCalls(page, "/api/telemetry-examples"); + + await page.goto("/telemetry"); + await waitForPageLoad(page); + + const runButton = page.getByRole("button", { name: /Run.*Request/i }); + await runButton.click(); + + await expect(page.getByText("Success")).toBeVisible({ timeout: 5000 }); + + const postRequests = requests.filter((r) => r.method() === "POST"); + expect(postRequests.length).toBeGreaterThan(0); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d63a0a85..b393819e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,6 +136,9 @@ importers: specifier: ^4.1.13 version: 4.1.13 devDependencies: + '@playwright/test': + specifier: ^1.50.1 + version: 1.58.0 '@types/node': specifier: ^20.0.0 version: 20.19.21 @@ -3000,8 +3003,8 @@ packages: resolution: {integrity: sha512-Z7x2dZOmznihvdvCvLKMl+nswtOSVxS2H2ocar+U9xx6iMfTp0VGIrX6a4xB1v80IwOPC7dT1LXIJrY70Xu3Jw==} engines: {node: ^20.19.0 || >=22.12.0} - '@oxc-project/types@0.108.0': - resolution: {integrity: sha512-7lf13b2IA/kZO6xgnIZA88sq3vwrxWk+2vxf6cc+omwYCRTiA5e63Beqf3fz/v8jEviChWWmFYBwzfSeyrsj7Q==} + '@oxc-project/types@0.110.0': + resolution: {integrity: sha512-6Ct21OIlrEnFEJk5LT4e63pk3btsI6/TusD/GStLi7wYlGJNOl1GI9qvXAnRAxQU9zqA2Oz+UwhfTOU2rPZVow==} '@oxc-project/types@0.93.0': resolution: {integrity: sha512-yNtwmWZIBtJsMr5TEfoZFDxIWV6OdScOpza/f5YxbqUMJk+j6QX3Cf3jgZShGEFYWQJ5j9mJ6jM0tZHu2J9Yrg==} @@ -3014,6 +3017,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.58.0': + resolution: {integrity: sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==} + engines: {node: '>=18'} + hasBin: true + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -3721,8 +3729,8 @@ packages: cpu: [arm64] os: [android] - '@rolldown/binding-android-arm64@1.0.0-beta.60': - resolution: {integrity: sha512-hOW6iQXtpG4uCW1zGK56+KhEXGttSkTp2ykncW/nkOIF/jOKTqbM944Q73HVeMXP1mPRvE2cZwNp3xeLIeyIGQ==} + '@rolldown/binding-android-arm64@1.0.0-rc.1': + resolution: {integrity: sha512-He6ZoCfv5D7dlRbrhNBkuMVIHd0GDnjJwbICE1OWpG7G3S2gmJ+eXkcNLJjzjNDpeI2aRy56ou39AJM9AD8YFA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] @@ -3733,8 +3741,8 @@ packages: cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.0.0-beta.60': - resolution: {integrity: sha512-vyDA4HXY2mP8PPtl5UE17uGPxUNG4m1wkfa3kAkR8JWrFbarV97UmLq22IWrNhtBPa89xqerzLK8KoVmz5JqCQ==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.1': + resolution: {integrity: sha512-YzJdn08kSOXnj85ghHauH2iHpOJ6eSmstdRTLyaziDcUxe9SyQJgGyx/5jDIhDvtOcNvMm2Ju7m19+S/Rm1jFg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] @@ -3745,8 +3753,8 @@ packages: cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-beta.60': - resolution: {integrity: sha512-WnxyqxAKP2BsxouwGY/RCF5UFw/LA4QOHhJ7VEl+UCelHokiwqNHRbryLAyRy3TE1FZ5eae+vAFcaetAu/kWLw==} + '@rolldown/binding-darwin-x64@1.0.0-rc.1': + resolution: {integrity: sha512-cIvAbqM+ZVV6lBSKSBtlNqH5iCiW933t1q8j0H66B3sjbe8AxIRetVqfGgcHcJtMzBIkIALlL9fcDrElWLJQcQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] @@ -3757,8 +3765,8 @@ packages: cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.0.0-beta.60': - resolution: {integrity: sha512-JtyWJ+zXOHof5gOUYwdTWI2kL6b8q9eNwqB/oD4mfUFaC/COEB2+47JMhcq78dey9Ahmec3DZKRDZPRh9hNAMQ==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.1': + resolution: {integrity: sha512-rVt+B1B/qmKwCl1XD02wKfgh3vQPXRXdB/TicV2w6g7RVAM1+cZcpigwhLarqiVCxDObFZ7UgXCxPC7tpDoRog==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] @@ -3769,8 +3777,8 @@ packages: cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.60': - resolution: {integrity: sha512-LrMoKqpHx+kCaNSk84iSBd4yVOymLIbxJQtvFjDN2CjQraownR+IXcwYDblFcj9ivmS54T3vCboXBbm3s1zbPQ==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.1': + resolution: {integrity: sha512-69YKwJJBOFprQa1GktPgbuBOfnn+EGxu8sBJ1TjPER+zhSpYeaU4N07uqmyBiksOLGXsMegymuecLobfz03h8Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -3781,8 +3789,8 @@ packages: cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.60': - resolution: {integrity: sha512-sqI+Vdx1gmXJMsXN3Fsewm3wlt7RHvRs1uysSp//NLsCoh9ZFEUr4ZzGhWKOg6Rvf+njNu/vCsz96x7wssLejQ==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.1': + resolution: {integrity: sha512-9JDhHUf3WcLfnViFWm+TyorqUtnSAHaCzlSNmMOq824prVuuzDOK91K0Hl8DUcEb9M5x2O+d2/jmBMsetRIn3g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -3793,8 +3801,8 @@ packages: cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.60': - resolution: {integrity: sha512-8xlqGLDtTP8sBfYwneTDu8+PRm5reNEHAuI/+6WPy9y350ls0KTFd3EJCOWEXWGW0F35ko9Fn9azmurBTjqOrQ==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.1': + resolution: {integrity: sha512-UvApLEGholmxw/HIwmUnLq3CwdydbhaHHllvWiCTNbyGom7wTwOtz5OAQbAKZYyiEOeIXZNPkM7nA4Dtng7CLw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -3805,8 +3813,8 @@ packages: cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.60': - resolution: {integrity: sha512-iR4nhVouVZK1CiGGGyz+prF5Lw9Lmz30Rl36Hajex+dFVFiegka604zBwzTp5Tl0BZnr50ztnVJ30tGrBhDr8Q==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.1': + resolution: {integrity: sha512-uVctNgZHiGnJx5Fij7wHLhgw4uyZBVi6mykeWKOqE7bVy9Hcxn0fM/IuqdMwk6hXlaf9fFShDTFz2+YejP+x0A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -3817,8 +3825,8 @@ packages: cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-beta.60': - resolution: {integrity: sha512-HbfNcqNeqxFjSMf1Kpe8itr2e2lr0Bm6HltD2qXtfU91bSSikVs9EWsa1ThshQ1v2ZvxXckGjlVLtah6IoslPg==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.1': + resolution: {integrity: sha512-T6Eg0xWwcxd/MzBcuv4Z37YVbUbJxy5cMNnbIt/Yr99wFwli30O4BPlY8hKeGyn6lWNtU0QioBS46lVzDN38bg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -3829,8 +3837,8 @@ packages: cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.0.0-beta.60': - resolution: {integrity: sha512-BiiamFcgTJ+ZFOUIMO9AHXUo9WXvHVwGfSrJ+Sv0AsTd2w3VN7dJGiH3WRcxKFetljJHWvGbM4fdpY5lf6RIvw==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.1': + resolution: {integrity: sha512-PuGZVS2xNJyLADeh2F04b+Cz4NwvpglbtWACgrDOa5YDTEHKwmiTDjoD5eZ9/ptXtcpeFrMqD2H4Zn33KAh1Eg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] @@ -3840,8 +3848,8 @@ packages: engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.0.0-beta.60': - resolution: {integrity: sha512-6roXGbHMdR2ucnxXuwbmQvk8tuYl3VGu0yv13KxspyKBxxBd4RS6iykzLD6mX2gMUHhfX8SVWz7n/62gfyKHow==} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.1': + resolution: {integrity: sha512-2mOxY562ihHlz9lEXuaGEIDCZ1vI+zyFdtsoa3M62xsEunDXQE+DVPO4S4x5MPK9tKulG/aFcA/IH5eVN257Cw==} engines: {node: '>=14.0.0'} cpu: [wasm32] @@ -3851,8 +3859,8 @@ packages: cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.60': - resolution: {integrity: sha512-JBOm8/DC/CKnHyMHoJFdvzVHxUixid4dGkiTqGflxOxO43uSJMpl77pSPXvzwZ/VXwqblU2V0/PanyCBcRLowQ==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.1': + resolution: {integrity: sha512-oQVOP5cfAWZwRD0Q3nGn/cA9FW3KhMMuQ0NIndALAe6obqjLhqYVYDiGGRGrxvnjJsVbpLwR14gIUYnpIcHR1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] @@ -3869,8 +3877,8 @@ packages: cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.60': - resolution: {integrity: sha512-MKF0B823Efp+Ot8KsbwIuGhKH58pf+2rSM6VcqyNMlNBHheOM0Gf7JmEu+toc1jgN6fqjH7Et+8hAzsLVkIGfA==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.1': + resolution: {integrity: sha512-Ydsxxx++FNOuov3wCBPaYjZrEvKOOGq3k+BF4BPridhg2pENfitSRD2TEuQ8i33bp5VptuNdC9IzxRKU031z5A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -3884,8 +3892,8 @@ packages: '@rolldown/pluginutils@1.0.0-beta.47': resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} - '@rolldown/pluginutils@1.0.0-beta.60': - resolution: {integrity: sha512-Jz4aqXRPVtqkH1E3jRDzLO5cgN5JwW+WG0wXGE4NiJd25nougv/AHzxmKCzmVQUYnxLmTM0M4wrZp+LlC2FKLg==} + '@rolldown/pluginutils@1.0.0-rc.1': + resolution: {integrity: sha512-UTBjtTxVOhodhzFVp/ayITaTETRHPUPYZPXQe0WU0wOgxghMojXxYjOiPOauKIYNWJAWS2fd7gJgGQK8GU8vDA==} '@rollup/rollup-android-arm-eabi@4.52.4': resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==} @@ -6706,6 +6714,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} @@ -8695,6 +8708,16 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + playwright-core@1.58.0: + resolution: {integrity: sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.0: + resolution: {integrity: sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==} + engines: {node: '>=18'} + hasBin: true + plop@4.0.4: resolution: {integrity: sha512-YdxtHWcPV8hDsszVPr4VQBVGNdn5ZQmEW+cZakZkuVeQHtENmrtY4AhuyoZW6s7ZjpmrS+llLQrfDgRKNQNsmg==} engines: {node: '>=18'} @@ -9641,8 +9664,8 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.0.0-beta.60: - resolution: {integrity: sha512-YYgpv7MiTp9LdLj1fzGzCtij8Yi2OKEc3HQtfbIxW4yuSgpQz9518I69U72T5ErPA/ATOXqlcisiLrWy+5V9YA==} + rolldown@1.0.0-rc.1: + resolution: {integrity: sha512-M3AeZjYE6UclblEf531Hch0WfVC/NOL43Cc+WdF3J50kk5/fvouHhDumSGTh0oRjbZ8C4faaVr5r6Nx1xMqDGg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -14578,7 +14601,7 @@ snapshots: '@oxc-project/runtime@0.92.0': {} - '@oxc-project/types@0.108.0': {} + '@oxc-project/types@0.110.0': {} '@oxc-project/types@0.93.0': {} @@ -14587,6 +14610,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.58.0': + dependencies: + playwright: 1.58.0 + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -15322,61 +15349,61 @@ snapshots: '@rolldown/binding-android-arm64@1.0.0-beta.41': optional: true - '@rolldown/binding-android-arm64@1.0.0-beta.60': + '@rolldown/binding-android-arm64@1.0.0-rc.1': optional: true '@rolldown/binding-darwin-arm64@1.0.0-beta.41': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-beta.60': + '@rolldown/binding-darwin-arm64@1.0.0-rc.1': optional: true '@rolldown/binding-darwin-x64@1.0.0-beta.41': optional: true - '@rolldown/binding-darwin-x64@1.0.0-beta.60': + '@rolldown/binding-darwin-x64@1.0.0-rc.1': optional: true '@rolldown/binding-freebsd-x64@1.0.0-beta.41': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-beta.60': + '@rolldown/binding-freebsd-x64@1.0.0-rc.1': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.41': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.60': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.1': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.41': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.60': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.1': optional: true '@rolldown/binding-linux-arm64-musl@1.0.0-beta.41': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.60': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.1': optional: true '@rolldown/binding-linux-x64-gnu@1.0.0-beta.41': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.60': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.1': optional: true '@rolldown/binding-linux-x64-musl@1.0.0-beta.41': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-beta.60': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.1': optional: true '@rolldown/binding-openharmony-arm64@1.0.0-beta.41': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-beta.60': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.1': optional: true '@rolldown/binding-wasm32-wasi@1.0.0-beta.41': @@ -15384,7 +15411,7 @@ snapshots: '@napi-rs/wasm-runtime': 1.0.7 optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.60': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.1': dependencies: '@napi-rs/wasm-runtime': 1.1.1 optional: true @@ -15392,7 +15419,7 @@ snapshots: '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.41': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.60': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.1': optional: true '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.41': @@ -15401,7 +15428,7 @@ snapshots: '@rolldown/binding-win32-x64-msvc@1.0.0-beta.41': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.60': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.1': optional: true '@rolldown/pluginutils@1.0.0-beta.38': {} @@ -15410,7 +15437,7 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.47': {} - '@rolldown/pluginutils@1.0.0-beta.60': {} + '@rolldown/pluginutils@1.0.0-rc.1': {} '@rollup/rollup-android-arm-eabi@4.52.4': optional: true @@ -18556,6 +18583,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -20975,6 +21005,14 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 + playwright-core@1.58.0: {} + + playwright@1.58.0: + dependencies: + playwright-core: 1.58.0 + optionalDependencies: + fsevents: 2.3.2 + plop@4.0.4(@types/node@24.7.2): dependencies: '@types/liftoff': 4.0.3 @@ -22033,7 +22071,7 @@ snapshots: robust-predicates@3.0.2: {} - rolldown-plugin-dts@0.16.11(rolldown@1.0.0-beta.60)(typescript@5.9.3): + rolldown-plugin-dts@0.16.11(rolldown@1.0.0-rc.1)(typescript@5.9.3): dependencies: '@babel/generator': 7.28.3 '@babel/parser': 7.28.5 @@ -22044,7 +22082,7 @@ snapshots: dts-resolver: 2.1.2 get-tsconfig: 4.12.0 magic-string: 0.30.19 - rolldown: 1.0.0-beta.60 + rolldown: 1.0.0-rc.1 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -22108,24 +22146,24 @@ snapshots: '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.41 '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.41 - rolldown@1.0.0-beta.60: + rolldown@1.0.0-rc.1: dependencies: - '@oxc-project/types': 0.108.0 - '@rolldown/pluginutils': 1.0.0-beta.60 + '@oxc-project/types': 0.110.0 + '@rolldown/pluginutils': 1.0.0-rc.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-beta.60 - '@rolldown/binding-darwin-arm64': 1.0.0-beta.60 - '@rolldown/binding-darwin-x64': 1.0.0-beta.60 - '@rolldown/binding-freebsd-x64': 1.0.0-beta.60 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.60 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.60 - '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.60 - '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.60 - '@rolldown/binding-linux-x64-musl': 1.0.0-beta.60 - '@rolldown/binding-openharmony-arm64': 1.0.0-beta.60 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.60 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.60 - '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.60 + '@rolldown/binding-android-arm64': 1.0.0-rc.1 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.1 + '@rolldown/binding-darwin-x64': 1.0.0-rc.1 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.1 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.1 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.1 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.1 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.1 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.1 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.1 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.1 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.1 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.1 rollup@4.52.4: dependencies: @@ -22773,8 +22811,8 @@ snapshots: diff: 8.0.2 empathic: 2.0.0 hookable: 5.5.3 - rolldown: 1.0.0-beta.60 - rolldown-plugin-dts: 0.16.11(rolldown@1.0.0-beta.60)(typescript@5.9.3) + rolldown: 1.0.0-rc.1 + rolldown-plugin-dts: 0.16.11(rolldown@1.0.0-rc.1)(typescript@5.9.3) semver: 7.7.3 tinyexec: 1.0.1 tinyglobby: 0.2.15