Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Playwright #2

Merged
merged 15 commits into from
Nov 13, 2024
Merged
79 changes: 79 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Playwright Tests
on:
pull_request:
branches: [main]

jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: latest

- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'

- name: Install dependencies
run: pnpm install

- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps

- name: Wait for Vercel Preview
id: wait_for_preview
run: |
# Maximum wait time in seconds (5 minutes)
max_wait=300
wait_interval=10
elapsed=0

while [ $elapsed -lt $max_wait ]; do
PR_NUMBER=${{ github.event.pull_request.number }}
DEPLOYMENTS=$(curl -s -H "Authorization: Bearer ${{ secrets.VERCEL_TOKEN }}" \
"https://api.vercel.com/v6/deployments?meta-githubPrId=$PR_NUMBER&state=READY&limit=1")

PREVIEW_URL=$(echo $DEPLOYMENTS | jq -r '.deployments[0].url')

if [ ! -z "$PREVIEW_URL" ] && [ "$PREVIEW_URL" != "null" ]; then
if [[ $PREVIEW_URL != http* ]]; then
PREVIEW_URL="https://$PREVIEW_URL"
fi

echo "PREVIEW_URL=$PREVIEW_URL" >> $GITHUB_OUTPUT
echo "Preview URL found: $PREVIEW_URL"
exit 0
fi

echo "Waiting for Vercel preview deployment... (${elapsed}s elapsed)"
sleep $wait_interval
elapsed=$((elapsed + wait_interval))
done

echo "Timeout waiting for Vercel preview deployment"
exit 1
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}

- name: Run Playwright tests
run: |
echo "Testing against URL: $PLAYWRIGHT_TEST_BASE_URL"
env:
PLAYWRIGHT_TEST_BASE_URL: ${{ steps.wait_for_preview.outputs.PREVIEW_URL }}

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: |
playwright-report/
test-results/
retention-days: 30
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ node_modules
/.cache
/build
.env
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
134 changes: 134 additions & 0 deletions e2e/decision-tree.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { test, expect } from '@playwright/test'
import {
checkHomepageContents,
loadExampleTree,
loadHomepage,
loadNewTree,
} from './helpers'

test.describe('pageload', () => {
test('should load landing page content as expected', async ({ page }) => {
await loadHomepage(page)
})

test('should load 404 page content as expected', async ({ page }) => {
await page.goto('/random-invalid-page')
await page.waitForLoadState('networkidle')
await expect(page).toHaveTitle(/404/i)
await expect(page.getByText('404')).toBeVisible()
await expect(page.getByText('Page not found.')).toBeVisible()
await expect(page.getByRole('link', { name: /Home/i })).toBeVisible()
await expect(page.getByText('Built by Lucas Silbernagel')).toBeVisible()
await page.click('text=Home')
await page.waitForLoadState('networkidle')
await expect(page).toHaveTitle(/Home/i)
await page.goto('/random-invalid-page')
await page.waitForLoadState('networkidle')
await expect(page).toHaveTitle(/404/i)
})
})

test.describe('decision tree functionality', () => {
test('should create a new decision tree on button click', async ({
page,
}) => {
await loadNewTree(page)
})

test('should load an example decision tree on button click', async ({
page,
}) => {
await loadExampleTree(page)
})

test('should copy decision tree URL on button click', async ({ page }) => {
await loadNewTree(page)
await page.evaluate(() => {
navigator.clipboard.writeText = async () => Promise.resolve()
})
await page.click('role=button[name="Share Tree"]')
await expect(
page.locator('div.text-sm.font-semibold', {
hasText: 'URL copied to clipboard!',
})
).toBeVisible()
})

test('should clear the decision tree and return to the landing page on button click', async ({
page,
}) => {
await loadExampleTree(page)
await page.click('role=button[name="Start Over"]')
await expect(
page.getByRole('heading', { name: 'Start Over' })
).toBeVisible()
await expect(page.getByText('Are you sure you want to')).toBeVisible()
await expect(page.getByRole('button', { name: 'Cancel' })).toBeVisible()
await expect(page.getByRole('button', { name: 'Continue' })).toBeVisible()
await page.click('role=button[name="Cancel"]')
await expect(
page.getByRole('heading', { name: 'Start Over' })
).not.toBeVisible()
await page.click('role=button[name="Start Over"]')
await page.click('role=button[name="Continue"]')
await page.waitForLoadState('networkidle')
await expect(page).toHaveTitle(/Home/i)
await checkHomepageContents(page)
})

test('should be able to edit the decision tree title', async ({ page }) => {
await loadNewTree(page)
await page.click('role=button[name="Decision Tree Title - edit"]')
await expect(
page.getByRole('button', { name: /Decision Tree Title/i })
).not.toBeVisible()
await expect(page.getByPlaceholder('Decision Tree Title')).toBeVisible()
await page.fill('[placeholder="Decision Tree Title"]', 'My fancy new title')
await page.keyboard.press('Enter')
await expect(page.getByPlaceholder('Decision Tree Title')).not.toBeVisible()
await expect(page.getByLabel('My fancy new title - edit')).toBeVisible()
})

test('should be able to edit decision node text', async ({ page }) => {
await loadNewTree(page)
await page
.getByLabel('Decision root: Yes or no?.')
.getByLabel('edit text')
.click()
await expect(page.getByPlaceholder('Yes or no?')).toBeVisible()
await page.fill('[placeholder="Yes or no?"]', 'Example decision node text.')
await page.keyboard.press('Enter')
await expect(page.getByPlaceholder('Yes or no?')).not.toBeVisible()
await expect(
page.getByLabel('Decision root: Example decision node text.')
).toBeVisible()
})

test('should be able to add a new decision node', async ({ page }) => {
await loadExampleTree(page)
await page
.getByLabel('Decision : Pick up a salad or')
.getByLabel('Add child nodes')
.click()
await expect(
page.getByLabel('Decision : No.').getByLabel('edit text')
).toBeVisible()
await expect(
page.getByLabel('Decision : Yes.').getByLabel('edit text')
).toBeVisible()
})

test('should be able to delete a decision node', async ({ page }) => {
await loadExampleTree(page)
await expect(
page.getByLabel('Decision : Pick up a salad or').getByLabel('edit text')
).toBeVisible()
await page
.getByLabel('Decision : Pick up a salad or')
.getByLabel('Delete node')
.click()
await expect(
page.getByLabel('Decision : Pick up a salad or').getByLabel('edit text')
).not.toBeVisible()
})
})
122 changes: 122 additions & 0 deletions e2e/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Page, expect } from '@playwright/test'

export const checkHomepageContents = async (page: Page) => {
await expect(page.getByRole('button', { name: /Example/i })).toBeVisible()
await expect(page.getByRole('button', { name: /New/i })).toBeVisible()
await expect(
page.getByText(
'A simple generator of shareable and accessible decision trees.'
)
).toBeVisible()
await expect(page.getByText('Built by Lucas Silbernagel')).toBeVisible()
}

export const loadHomepage = async (page: Page) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
await expect(page).toHaveTitle(/Home/i)
await checkHomepageContents(page)
}

export const loadNewTree = async (page: Page) => {
await loadHomepage(page)
await page.click('text=New')
await expect(page.getByRole('button', { name: /Share Tree/i })).toBeVisible()
await expect(page.getByRole('button', { name: /Start Over/i })).toBeVisible()
await expect(
page.getByRole('button', { name: /Decision Tree Title/i })
).toBeVisible()
await expect(
page.getByLabel('Decision root: Yes or no?.').getByLabel('edit text')
).toBeVisible()
await expect(
page.getByLabel('Decision : No.').getByLabel('edit text')
).toBeVisible()
await expect(
page.getByLabel('Decision : No.').getByLabel('Add child nodes')
).toBeVisible()
await expect(
page.getByLabel('Decision : Yes.').getByLabel('Add child nodes')
).toBeVisible()
await expect(
page.getByLabel('Decision : Yes.').getByLabel('edit text')
).toBeVisible()
await expect(page.getByText('Built by Lucas Silbernagel')).toBeVisible()
}

export const loadExampleTree = async (page: Page) => {
await loadHomepage(page)
await page.click('text=Example')
await expect(page.getByRole('button', { name: /Share Tree/i })).toBeVisible()
await expect(page.getByRole('button', { name: /Start Over/i })).toBeVisible()
await expect(
page.getByRole('button', { name: /What should you have for lunch?/i })
).toBeVisible()
await expect(
page.getByLabel('Decision root: Are you at home?').getByLabel('edit text')
).toBeVisible()
await expect(
page
.getByLabel('Decision : Do you want to go to a sit-down restaurant?')
.getByLabel('edit text')
).toBeVisible()
await expect(
page
.getByLabel('Decision : Do you have ingredients to make a sandwich?')
.getByLabel('edit text')
).toBeVisible()
await expect(
page.getByLabel('Decision : Pick up a salad or').getByLabel('Delete node')
).toBeVisible()
await expect(
page
.getByLabel('Decision : Pick up a salad or')
.getByLabel('Add child nodes')
).toBeVisible()
await expect(page.getByText('Built by Lucas Silbernagel')).toBeVisible()
await expect(
page.getByRole('button', { name: /Back to start/i })
).not.toBeVisible()
const scrollableArea = page.getByLabel('Decision tree navigation area')
await scrollableArea.waitFor({ state: 'attached' })
let scrollSuccess = false
for (let i = 0; i < 3; i++) {
try {
// Simple scroll first
await scrollableArea.evaluate((div) => {
div.scrollTop = div.scrollHeight
})
// Wait a bit for scroll to settle
await page.waitForTimeout(100)
// Verify scroll position
const isScrolled = await scrollableArea.evaluate((div) => {
const maxScroll = div.scrollHeight - div.clientHeight
return Math.abs(div.scrollTop - maxScroll) < 1
})
if (isScrolled) {
scrollSuccess = true
break
}
} catch (error) {
if (i === 2) throw error // On last attempt, throw the error
await page.waitForTimeout(100) // Wait before retry
}
}
if (!scrollSuccess) {
throw new Error('Failed to scroll to bottom')
}
// Wait for button to be visible with retry
await expect(
page.getByRole('button', { name: /Back to start/i })
).toBeVisible({ timeout: 5000 })
await expect(
page.getByLabel('Decision root: Are you at home?').getByLabel('edit text')
).not.toBeInViewport()
await page.getByRole('button', { name: /Back to start/i }).click({
timeout: 5000,
force: true, // Use force: true to click even if element is moving
})
await expect(
page.getByLabel('Decision root: Are you at home?').getByLabel('edit text')
).toBeInViewport()
}
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"start": "remix-serve ./build/server/index.js",
"typecheck": "tsc",
"test-unit": "vitest"
"test-unit": "vitest",
"test-e2e": " pnpm exec playwright test",
"test-e2e-ui": "pnpm exec playwright test --ui"
},
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.2",
Expand All @@ -30,13 +32,15 @@
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@playwright/test": "^1.48.2",
"@remix-run/dev": "^2.12.1",
"@remix-run/testing": "^2.13.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/dompurify": "^3.0.5",
"@types/node": "^22.9.0",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.7.4",
Expand Down
Loading
Loading