diff --git a/.github/workflows/ci-deploy-cloudflare.yaml b/.github/workflows/ci-deploy-cloudflare.yaml index 1f93634f..d30995e6 100644 --- a/.github/workflows/ci-deploy-cloudflare.yaml +++ b/.github/workflows/ci-deploy-cloudflare.yaml @@ -28,3 +28,14 @@ jobs: - name: Build the app run: pnpm build + + - name: Verify build + run: | + if [ ! -f dist/_worker.js ]; then + echo "dist/_worker.js not found" + exit 1 + fi + if [ ! -f dist/index.html ]; then + echo "dist/index.html not found" + exit 1 + fi diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml new file mode 100644 index 00000000..697e35c5 --- /dev/null +++ b/.github/workflows/ci-test-e2e.yml @@ -0,0 +1,53 @@ +name: E2E Tests + +on: + deployment_status: + +jobs: + run-e2es: + if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success' + runs-on: ubuntu-latest + timeout-minutes: 60 + defaults: + run: + working-directory: packages/app-client + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + corepack: true + cache: 'pnpm' + + - name: Get Playwright version + id: playwright-version + run: echo "PLAYWRIGHT_VERSION=$(jq -r .dependencies.playwright package.json)" >> "$GITHUB_OUTPUT" + + - name: Install dependencies + run: pnpm i + working-directory: ./ + + - name: Restore Playwright browsers from cache + uses: actions/cache@v3 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}-${{ hashFiles('**/playwright.config.ts') }} + restore-keys: | + ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}- + ${{ runner.os }}-playwright- + + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + + - name: Run e2e tests + run: pnpm test:e2e + env: + BASE_URL: ${{ github.event.deployment_status.environment_url }} + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 15 diff --git a/packages/app-client/.gitignore b/packages/app-client/.gitignore index ba6e890d..7922244c 100644 --- a/packages/app-client/.gitignore +++ b/packages/app-client/.gitignore @@ -27,4 +27,9 @@ gitignore .DS_Store Thumbs.db -cache \ No newline at end of file +cache + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/packages/app-client/e2e-tests/createand-view-note.e2e.test.ts b/packages/app-client/e2e-tests/createand-view-note.e2e.test.ts new file mode 100644 index 00000000..8ecbbcad --- /dev/null +++ b/packages/app-client/e2e-tests/createand-view-note.e2e.test.ts @@ -0,0 +1,31 @@ +import { expect, test } from '@playwright/test'; + +test('Can create and view a note', async ({ page }) => { + await page.goto('/'); + + await expect(page).toHaveTitle('Enclosed - Send private and secure notes'); + + // Write a note with a password and delete after reading + await page.getByTestId('note-content').fill('Hello, World!'); + await page.getByTestId('note-password').fill('my-cat-is-cute'); + await page.getByTestId('delete-after-reading').click(); + + await page.getByTestId('create-note').click(); + const noteUrl = await page.getByTestId('note-url').inputValue(); + + expect(noteUrl).toBeDefined(); + + await page.goto(noteUrl); + + await page.getByTestId('note-password-prompt').fill('my-cat-is-cute'); + await page.getByTestId('note-password-submit').click(); + + const noteContent = await page.getByTestId('note-content-display').textContent(); + + expect(noteContent).toBe('Hello, World!'); + + // Refresh the page and check if the note is still there + await page.reload(); + + await expect(page.getByText('Note not found')).toBeVisible(); +}); diff --git a/packages/app-client/index.html b/packages/app-client/index.html index d1ad5070..c5cbb263 100644 --- a/packages/app-client/index.html +++ b/packages/app-client/index.html @@ -8,7 +8,7 @@ - + @@ -17,14 +17,14 @@ - + - + diff --git a/packages/app-client/package.json b/packages/app-client/package.json index f1c1e71d..0373fbad 100644 --- a/packages/app-client/package.json +++ b/packages/app-client/package.json @@ -23,6 +23,7 @@ "test": "pnpm run test:unit", "test:unit": "vitest run", "test:unit:watch": "vitest watch", + "test:e2e": "playwright test", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -44,7 +45,9 @@ "devDependencies": { "@antfu/eslint-config": "^3.0.0", "@iconify-json/tabler": "^1.1.120", + "@playwright/test": "^1.46.1", "@types/lodash-es": "^4.17.12", + "@types/node": "^22.5.0", "eslint": "^9.10.0", "jsdom": "^25.0.0", "typescript": "^5.3.3", diff --git a/packages/app-client/playwright.config.ts b/packages/app-client/playwright.config.ts new file mode 100644 index 00000000..3d48aa17 --- /dev/null +++ b/packages/app-client/playwright.config.ts @@ -0,0 +1,52 @@ +import process from 'node:process'; +import { defineConfig, devices } from '@playwright/test'; + +const baseURL = process.env.BASE_URL ?? 'http://localhost:3000/'; +const isCI = Boolean(process.env.CI); + +/** + * 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: isCI, + /* Retry on CI only */ + retries: isCI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: isCI ? 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, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + testIdAttribute: 'data-test-id', + locale: 'en-GB', + timezoneId: 'Europe/Paris', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], +}); diff --git a/packages/app-client/src/modules/notes/components/note-password-field.tsx b/packages/app-client/src/modules/notes/components/note-password-field.tsx index b765fa7e..09957e2d 100644 --- a/packages/app-client/src/modules/notes/components/note-password-field.tsx +++ b/packages/app-client/src/modules/notes/components/note-password-field.tsx @@ -4,7 +4,7 @@ import { TextField } from '@/modules/ui/components/textfield'; import { type Component, createSignal } from 'solid-js'; import { createRandomPassword } from '../notes.models'; -export const NotePasswordField: Component<{ getPassword: () => string; setPassword: (value: string) => void }> = (props) => { +export const NotePasswordField: Component<{ getPassword: () => string; setPassword: (value: string) => void; dataTestId?: string } > = (props) => { const [getShowPassword, setShowPassword] = createSignal(false); const { t } = useI18n(); @@ -17,7 +17,7 @@ export const NotePasswordField: Component<{ getPassword: () => string; setPasswo return (
- props.setPassword(e.currentTarget.value)} class="border-none shadow-none focus-visible:ring-none" type={getShowPassword() ? 'text' : 'password'} /> + props.setPassword(e.currentTarget.value)} class="border-none shadow-none focus-visible:ring-none" type={getShowPassword() ? 'text' : 'password'} data-test-id={props.dataTestId} />