diff --git a/.gitignore b/.gitignore index e63c4d922..2d49680a8 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,5 @@ frontend/static/webfonts-preview .turbo frontend/.env.sentry-build-plugin +test-results +playwright-report diff --git a/e2e-tests/page-load.spec.ts b/e2e-tests/page-load.spec.ts new file mode 100644 index 000000000..85080b010 --- /dev/null +++ b/e2e-tests/page-load.spec.ts @@ -0,0 +1,143 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Monkeytype Page Load and Words Generation", () => { + test("should load the page and generate words in the #words element", async ({ + page, + }) => { + // Navigate to the homepage + await page.goto("/"); + + // Wait for the page to be fully loaded + await page.waitForLoadState("networkidle"); + + // Check if the page title is correct + await expect(page).toHaveTitle(/monkeytype/i); + + // Wait for the app container to be visible + await expect(page.locator("#app")).toBeVisible(); + + // Check if the test page is accessible (it should be the default page) + const testPage = page.locator(".pageTest"); + await expect(testPage).toBeVisible(); + + // Wait for the words container to be visible + const wordsContainer = page.locator("#words"); + await expect(wordsContainer).toBeVisible(); + + // Wait for words to be generated and displayed + // Give it more time as the words might take a moment to load + await page.waitForTimeout(2000); + + // Check that there are actual word elements inside the words container + // Monkeytype typically generates words as spans or divs inside the #words container + const wordElements = wordsContainer.locator("*"); + await expect(wordElements.first()).toBeVisible(); + + // Verify that the words container has some text content + const wordsText = await wordsContainer.textContent(); + expect(wordsText).toBeTruthy(); + expect(wordsText!.trim().length).toBeGreaterThan(0); + + // Log the actual content for debugging + console.log(`Generated words content: "${wordsText}"`); + console.log(`Word count: ${wordsText!.trim().split(/\s+/).length}`); + + // Check that there is meaningful content (at least some characters) + // We'll be more lenient about the word count since the structure might be different + expect(wordsText!.trim().length).toBeGreaterThan(10); + }); + + test("should have essential typing elements present", async ({ page }) => { + await page.goto("/"); + await page.waitForLoadState("networkidle"); + + // Wait a bit for the page to fully initialize + await page.waitForTimeout(2000); + + // Handle cookies modal if present + const cookiesModal = page.locator("#cookiesModal"); + if (await cookiesModal.isVisible()) { + const acceptButton = page.locator("#cookiesModal button").first(); + if (await acceptButton.isVisible()) { + await acceptButton.click(); + await page.waitForTimeout(500); + } + } + + // Check if the input field is present + const wordsInput = page.locator("#wordsInput"); + await expect(wordsInput).toBeVisible(); + + // Check if the caret is visible (indicates the test is ready) + const caret = page.locator("#caret"); + await expect(caret).toBeVisible(); + + // Verify the typing test words wrapper is ready (not the replay one) + const wordsWrapper = page.locator("#typingTest #wordsWrapper"); + await expect(wordsWrapper).toBeVisible(); + }); + + test("should display test configuration options", async ({ page }) => { + await page.goto("/"); + await page.waitForLoadState("networkidle"); + + // Handle cookies modal if present + const cookiesModal = page.locator("#cookiesModal"); + if (await cookiesModal.isVisible()) { + const acceptButton = page.locator("#cookiesModal button").first(); + if (await acceptButton.isVisible()) { + await acceptButton.click(); + await page.waitForTimeout(500); + } + } + + // Check if test configuration is visible + const testConfig = page.locator("#testConfig"); + await expect(testConfig).toBeVisible(); + + // Check for mode buttons (time, words, quote, etc.) + await expect(page.locator('button[mode="time"]')).toBeVisible(); + await expect(page.locator('button[mode="words"]')).toBeVisible(); + await expect(page.locator('button[mode="quote"]')).toBeVisible(); + + // Check for time configuration buttons + await expect(page.locator('button[timeConfig="15"]')).toBeVisible(); + await expect(page.locator('button[timeConfig="30"]')).toBeVisible(); + await expect(page.locator('button[timeConfig="60"]')).toBeVisible(); + }); + + test("should allow basic typing interaction", async ({ page }) => { + await page.goto("/"); + await page.waitForLoadState("networkidle"); + + // Wait for initialization + await page.waitForTimeout(2000); + + // Handle cookies modal if present + const cookiesModal = page.locator("#cookiesModal"); + if (await cookiesModal.isVisible()) { + const acceptButton = page.locator("#cookiesModal button").first(); + if (await acceptButton.isVisible()) { + await acceptButton.click(); + await page.waitForTimeout(500); + } + } + + // Verify that the typing interface is ready + const wordsInput = page.locator("#wordsInput"); + await expect(wordsInput).toBeVisible(); + + // Type a simple character to test basic functionality + await page.keyboard.type("a"); + await page.keyboard.type("s"); + await page.keyboard.type("d"); + await page.keyboard.type("f"); + + //expect (wordsInput).toHaveValue("asdf") + await expect(wordsInput).toHaveValue(" asdf"); + + // The fact that we got here without errors means basic typing setup is working + // We don't need to verify the exact input value as that depends on the app's logic + console.log("Basic typing interaction test completed successfully"); + }); +}); diff --git a/package.json b/package.json index 1e5d7db53..ad9781a04 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,9 @@ "test-be": "turbo run test integration-test --filter @monkeytype/backend", "test-fe": "turbo run test --filter @monkeytype/frontend", "test-pkg": "turbo run test --filter=\"./packages/*\"", + "test-e2e": "playwright test", + "test-e2e-ui": "playwright test --ui", + "test-e2e-headed": "playwright test --headed", "dev": "turbo run dev --force", "dev-be": "turbo run dev --force --filter @monkeytype/backend", "dev-fe": "turbo run dev --force --filter @monkeytype/frontend", @@ -67,6 +70,7 @@ "@commitlint/cli": "17.7.1", "@commitlint/config-conventional": "19.2.2", "@monkeytype/release": "workspace:*", + "@playwright/test": "1.55.0", "@vitest/coverage-v8": "3.2.4", "conventional-changelog": "6.0.0", "eslint": "8.57.1", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..ab511e35f --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,71 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: "./e2e-tests", + /* 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: "html", + /* 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"] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: "pnpm dev-fe", + url: "http://localhost:3000", + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58ac5eb86..edfc7f9d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@monkeytype/release': specifier: workspace:* version: link:packages/release + '@playwright/test': + specifier: 1.55.0 + version: 1.55.0 '@vitest/coverage-v8': specifier: 3.2.4 version: 3.2.4(vitest@3.2.4(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0)) @@ -2600,6 +2603,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.55.0': + resolution: {integrity: sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==} + engines: {node: '>=18'} + hasBin: true + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -5344,6 +5352,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} @@ -7642,6 +7655,16 @@ packages: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} + playwright-core@1.55.0: + resolution: {integrity: sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.55.0: + resolution: {integrity: sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -11995,6 +12018,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.55.0': + dependencies: + playwright: 1.55.0 + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -14120,10 +14147,6 @@ snapshots: dependencies: ms: 2.1.2 - debug@4.3.6: - dependencies: - ms: 2.1.2 - debug@4.3.6(supports-color@5.5.0): dependencies: ms: 2.1.2 @@ -15455,6 +15478,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -16657,7 +16683,7 @@ snapshots: chalk: 5.2.0 cli-truncate: 3.1.0 commander: 10.0.1 - debug: 4.3.6 + debug: 4.3.6(supports-color@5.5.0) execa: 7.2.0 lilconfig: 2.1.0 listr2: 5.0.8 @@ -18063,6 +18089,14 @@ snapshots: pirates@4.0.6: {} + playwright-core@1.55.0: {} + + playwright@1.55.0: + dependencies: + playwright-core: 1.55.0 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} polished@4.3.1: