diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..e8576fb --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,50 @@ +name: Playwright Tests + +on: + push: + branches: + - main + pull_request: + branches: + - '**' + +jobs: + test: + timeout-minutes: 5 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: Install dependencies + run: npm install -g pnpm && pnpm install + + - name: Get Playwright Version + id: playwright-version + working-directory: app + run: echo "PLAYWRIGHT_VERSION=$(pnpm exec playwright --version | sed 's/Version //')" >> $GITHUB_ENV + + - name: Cache Playwright Browsers + uses: actions/cache@v3 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} + restore-keys: | + ${{ runner.os }}-playwright- + + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + working-directory: app + + - name: Run Playwright tests + run: pnpm test-e2e + working-directory: app + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: app/playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 752d22f..fff0e45 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,10 @@ coverage/ dist/ tsconfig.tsbuildinfo .DS_Store -.env \ No newline at end of file +.env + +# Playwright +test-results/ +playwright-report/ +blob-report/ +playwright/.cache/ diff --git a/app/package.json b/app/package.json index f504622..e73baee 100644 --- a/app/package.json +++ b/app/package.json @@ -45,7 +45,9 @@ "yjs": "^13.6.15" }, "devDependencies": { + "@playwright/test": "^1.44.1", "@types/jsdom": "^21.1.7", + "@types/node": "^20.11.24", "@types/react": "^18.2.61", "@types/react-dom": "^18.2.19", "@vitejs/plugin-react": "^4.2.1", @@ -58,7 +60,8 @@ "scripts": { "build": "vite build", "dev": "vite dev", - "test": "vitest run" + "test": "vitest run", + "test-e2e": "playwright test" }, "browserslist": [ ">0.2%", diff --git a/app/playwright.config.ts b/app/playwright.config.ts new file mode 100644 index 0000000..45e9a10 --- /dev/null +++ b/app/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + + /* Run tests in files in parallel */ + fullyParallel: true, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + contextOptions: { + permissions: ['clipboard-read', 'clipboard-write'], + }, + }, + }, + + { + name: 'webkit', + use: { + ...devices['Desktop Safari'], + contextOptions: { + permissions: ['clipboard-read'], + }, + }, + }, + + /* Test against mobile viewports. */ + { + name: 'Mobile Chrome', + use: { + ...devices['Pixel 5'], + contextOptions: { + permissions: ['clipboard-read', 'clipboard-write'], + }, + }, + }, + { + name: 'Mobile Safari', + use: { + ...devices['iPhone 12'], + contextOptions: { + permissions: ['clipboard-read'], + }, + }, + }, + ], + + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + + testDir: './tests-e2e', + + /* 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:5173', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'pnpm dev', + reuseExistingServer: !process.env.CI, + url: 'http://localhost:5173', + }, + + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, +}); diff --git a/app/tests-e2e/smoke.spec.ts b/app/tests-e2e/smoke.spec.ts new file mode 100644 index 0000000..422b5a3 --- /dev/null +++ b/app/tests-e2e/smoke.spec.ts @@ -0,0 +1,32 @@ +import { expect, test } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('/'); + + await expect(page).toHaveTitle(/Collabify/); +}); + +test('can invite collaborator and share text', async ({ browser }) => { + const hostContext = await browser.newContext(); + const collaboratorContext = await browser.newContext(); + + const hostPage = await hostContext.newPage(); + const collaboratorPage = await collaboratorContext.newPage(); + + // Start a new session as host + await hostPage.goto('/new'); + await hostPage.getByRole('textbox').fill('Hello world'); + + // Invite the collaborator + await hostPage + .locator('button') + .filter({ hasText: 'Copy invite URL' }) + .click(); + const joinUrl = await hostPage.evaluate(() => navigator.clipboard.readText()); + await collaboratorPage.goto(joinUrl); + + // The collaborator should see the text from the host + await expect(collaboratorPage.getByRole('textbox')).toContainText( + 'Hello world', + ); +}); diff --git a/app/vite.config.ts b/app/vite.config.ts index a9f82e1..7c3c48e 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -9,4 +9,7 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), }, }, + test: { + exclude: ['node_modules', 'tests-e2e'], + }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d2d391..f099c1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,9 +144,15 @@ importers: specifier: ^13.6.15 version: 13.6.16 devDependencies: + '@playwright/test': + specifier: ^1.44.1 + version: 1.44.1 '@types/jsdom': specifier: ^21.1.7 version: 21.1.7 + '@types/node': + specifier: ^20.11.24 + version: 20.14.2 '@types/react': specifier: ^18.2.61 version: 18.3.3 @@ -699,6 +705,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.44.1': + resolution: {integrity: sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==} + engines: {node: '>=16'} + hasBin: true + '@radix-ui/number@1.0.1': resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} @@ -2077,6 +2088,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} @@ -2762,6 +2778,16 @@ packages: pkg-types@1.1.1: resolution: {integrity: sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==} + playwright-core@1.44.1: + resolution: {integrity: sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==} + engines: {node: '>=16'} + hasBin: true + + playwright@1.44.1: + resolution: {integrity: sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==} + engines: {node: '>=16'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -4326,6 +4352,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.44.1': + dependencies: + playwright: 1.44.1 + '@radix-ui/number@1.0.1': dependencies: '@babel/runtime': 7.24.5 @@ -5929,6 +5959,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -6583,6 +6616,14 @@ snapshots: mlly: 1.7.1 pathe: 1.1.2 + playwright-core@1.44.1: {} + + playwright@1.44.1: + dependencies: + playwright-core: 1.44.1 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} possible-typed-array-names@1.0.0: {}